diff options
663 files changed, 32975 insertions, 10539 deletions
diff --git a/.editorconfig b/.editorconfig index 109d855..1c59d9d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,4 @@ max_line_length = 120 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 @@ -0,0 +1,4 @@ +#SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe> +# +#SPDX-License-Identifier: CC0-1.0 +use nix diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..dfc9c09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +name: Mod Bug Report +description: Report a bug or crash in Firmament +labels: + - bug +body: + - type: input + id: version + attributes: + label: Firmament Version + description: Which version of Firmament are you using. Please provide the full version name (found in the file name, or the mod list). Please **don't** say latest. + validations: + required: true + - type: textarea + id: description + attributes: + label: Bug Description + description: Please describe the bug you encountered and what you did before you encountered it. + validations: + required: true + - type: textarea + attributes: + label: Minecraft Log + description: Please provide the complete Minecraft Log. Please do not truncate your log, or extract only parts. Earlier parts of the log can contain important information. + render: plain text + placeholder: If you are worried about your log containing private information, you can use an external uploader like https://mclo.gs/ to strip all personal information, and paste the link in here instead. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a3a11ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +blank_issues_enabled: true +contact_links: + - name: Discord Server + url: https://discord.gg/64pFP94AWA + about: For non bug support, check out the Discord. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..29fa766 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: CC0-1.0 + +name: Feature Request +description: Request a new feature or for a feature to be expanded. +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Thank you for expressing your interest in a new Firmament feature. + Please keep in mind that not every feature is a good fit for Firmament, + *especially* if there is already another mod for Firmaments versions + which covers that feature very well. It doesn't hurt to ask, but please + understand that even features I want to implement, might take some time + to be fully realized. + + Before creating a feature request, please check if the feature complies + with [HyPixel rules](https://hypixel.net/skyblock-rules). + - type: textarea + attributes: + label: Describe the feature + description: Please describe the feature you are looking for. + validations: + required: true + - type: input + attributes: + label: Other mods + description: Do you know of other mods that have this feature already? If so, please list them here. diff --git a/.github/composite/gradle/action.yml b/.github/composite/gradle/action.yml index 5f7a260..003fcd0 100644 --- a/.github/composite/gradle/action.yml +++ b/.github/composite/gradle/action.yml @@ -6,21 +6,23 @@ name: "Run Gradle on a filter=tree:0 checkout" runs: using: composite steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 name: Checkout repository with: fetch-tags: true fetch-depth: 0 filter: 'tree:0' - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Set up gradle cache - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: + cache-read-only: false add-job-summary-as-pr-comment: 'on-failure' + dependency-graph: 'generate-and-submit' - name: Prepare unpacked Jars run: | ./gradlew unpackAllJars diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1daf9d..c40a038 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 name: Checkout repository with: fetch-tags: true @@ -24,16 +24,19 @@ jobs: - uses: ./.github/composite/gradle - name: Build with gradle run: | - ./gradlew remapJar --scan + ./gradlew assemble --scan - name: Move build artifact around and print check sum run: | - rm -f build/libs/*sources*.jar + rm -f build/libs/*sources*.jar build/libs/*-api.jar sha256sum build/libs/*.jar - name: Upload build artifact uses: actions/upload-artifact@v4 with: name: built-mod-jar path: build/libs/Firmament-*.jar + exclude: | + build/libs/*-sources.jar + build/libs/*-api.jar test: name: Test @@ -55,10 +58,10 @@ jobs: name: Upload runs-on: ubuntu-latest needs: build - if: ${{ 'push' == github.event_name && 'master' == github.ref_name && '637563904' == github.repository_id }} + if: ${{ 'push' == github.event_name && ('master' == github.ref_name || startsWith(github.ref_name, 'mc-')) && '637563904' == github.repository_id }} steps: - name: Download generated artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: built-mod-jar - name: Upload to discord @@ -67,3 +70,39 @@ jobs: curl "$WEBHOOK_URL" -X POST -H "Content-type: multipart/form-data" --form "files[0]=@$(echo *.jar)" env: WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} + + prepare-release: + name: Prepare a release + runs-on: ubuntu-22.04 + needs: build + permissions: + contents: write + if: ${{ 'push' == github.event_name && startsWith(github.ref, 'refs/tags/') }} + steps: + - uses: actions/checkout@v5 + name: Checkout repository + with: + fetch-tags: true + fetch-depth: 0 + filter: 'tree:0' + + - name: Download generated artifact + uses: actions/download-artifact@v5 + with: + name: built-mod-jar + - name: Generate changelog + run: ./docs/generate-changelog.sh > changelog.md + + - name: Upload release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create --draft -t "Firmament ${GITHUB_REF#refs/tags/}" "${GITHUB_REF#refs/tags/}" -F "changelog.md" "*.jar" + + - name: Notify discord + env: + ACTOR: ${{ github.actor }} + WEBHOOK: ${{ secrets.CONFIDENTIAL_WEBHOOK }} + REPO: ${{ github.repository }} + run: | + curl -X POST "$WEBHOOK" --data "$(jq -n --arg r "$REPO" --arg v "${GITHUB_REF#refs/tags/}" --arg a "$ACTOR" '{"content": ("Created release preview, ready for publishing." + $v+"\n\nSee the draft at https://github.com/"+$r+"/releases/"), "username": $a, "avatar_url": ("https://github.com/" + $a + ".png")}')" -H 'content-type: application/json' diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 844c64a..45234cb 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -10,7 +10,7 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout repository - - uses: gradle/actions/wrapper-validation@v3 + - uses: gradle/actions/wrapper-validation@v4 name: Validate wrappers diff --git a/.github/workflows/meta-checks.yml b/.github/workflows/meta-checks.yml index 8a15be2..ac71229 100644 --- a/.github/workflows/meta-checks.yml +++ b/.github/workflows/meta-checks.yml @@ -12,7 +12,7 @@ jobs: name: Run REUSE Compliance check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 name: Checkout Repository - name: REUSE Compliance Check uses: fsfe/reuse-action@140e81ab76b30d9c5641b7b11e28222b3a11f8f9 @@ -23,8 +23,8 @@ jobs: outputs: hooks: ${{ steps.generate-matrix.outputs.hooks }} steps: - - uses: actions/checkout@v4 - name: Checkout Repository+ + - uses: actions/checkout@v5 + name: Checkout Repository - name: Generate matrix id: generate-matrix run: | @@ -41,9 +41,9 @@ jobs: hook: ${{ fromJSON(needs.pre-commit-generate.outputs.hooks) }} name: 'PRE-Commit: ${{ matrix.hook }}' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 name: Checkout Repository - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Setup Python - name: Check ${{ matrix.hook }} uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish-github-to-modrinth.yml b/.github/workflows/publish-github-to-modrinth.yml index 18ecf4a..7b966ab 100644 --- a/.github/workflows/publish-github-to-modrinth.yml +++ b/.github/workflows/publish-github-to-modrinth.yml @@ -19,6 +19,7 @@ jobs: steps: - run: | gh release download "$TAG" -p '*.jar' --repo "$REPO" + rm -f *-sources.jar *-api.jar *-api-sources.jar env: TAG: ${{ github.event.release.tag_name }} REPO: ${{ github.repository }} diff --git a/.github/workflows/publish-website.yml b/.github/workflows/publish-website.yml index f492738..191de25 100644 --- a/.github/workflows/publish-website.yml +++ b/.github/workflows/publish-website.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest name: Astro Build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 name: Checkout Code - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 23 - name: Setup PNPM @@ -33,7 +33,7 @@ jobs: name: Deploy Website runs-on: ubuntu-latest needs: build-website - if: ${{ 'push' == github.event_name && 'master' == github.ref_name && '637563904' == github.repository_id }} + if: ${{ 'push' == github.event_name && github.ref_name == github.event.repository.default_branch && '637563904' == github.repository_id }} environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -41,7 +41,7 @@ jobs: pages: write id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 name: Download web artifact - name: Deploy github pages id: deployment @@ -6,8 +6,8 @@ .properties /.gradle /build/ -/buildSrc/.gradle -/buildSrc/build/ +/build-logic/.gradle +/build-logic/build/ /*/build .kotlin @@ -31,3 +31,5 @@ gradle-app.setting /logs/ *.xsd *.png~ +*.kra +.direnv diff --git a/LICENSES/LGPL-3.0-or-later.txt b/LICENSES/LGPL-3.0-or-later.txt new file mode 100644 index 0000000..513d1c0 --- /dev/null +++ b/LICENSES/LGPL-3.0-or-later.txt @@ -0,0 +1,304 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/> + +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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +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: + + <program> Copyright (C) <year> <name of author> + 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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/philosophy/why-not-lgpl.html>. @@ -4,24 +4,31 @@ SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe> SPDX-License-Identifier: CC0-1.0 --> + + +<div align="center"> + # Firmament -<small><i>Powered by NEU</i></small> + + +<hr> [](https://hypixel.net/threads/firmament-a-skyblock-mod-for-1-20-1.5446366/) [](https://discord.gg/64pFP94AWA) [](https://modrinth.com/mod/firmament) -[](https://github.com/romangraef/firmament/releases) +[](https://github.com/nea89o/firmament/releases) + +</div> + ## Currently working features - Item List of all SkyBlock Items -- Grouping Items that belong together like minions - Recipe Viewer for Crafting Recipes - Recipe Viewer for Forge Recipes - ... as well as many more custom recipe types. - NPC waypoints -- Image Preview in chat - A storage overview as well as a full storage overlay - A crafting overlay when clicking the "Move Item" plus in a crafting recipe - Cursor position saver @@ -38,11 +45,15 @@ SPDX-License-Identifier: CC0-1.0 Firmament needs the following libraries to work: +- [Fabric API](https://modrinth.com/mod/fabric-api) +- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin) + +As well as (for the item list): +- - [RoughlyEnoughItems](https://modrinth.com/mod/rei) - [Architectury](https://modrinth.com/mod/architectury-api) - [Cloth Config](https://modrinth.com/mod/cloth-config) -- [Fabric API](https://modrinth.com/mod/fabric-api) -- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin) + You can download Firmament itself on [Modrinth](https://modrinth.com/mod/firmament) or on [GitHub](https://github.com/romangraef/firmament/releases). @@ -23,7 +23,12 @@ SPDX-License-Identifier = "GPL-3.0-or-later" SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"] [[annotations]] -path = "translations/*.json" +path = "translations/**/*.json" +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"] + +[[annotations]] +path = "**/fabric.mod.json" SPDX-License-Identifier = "CC0-1.0" SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"] @@ -47,3 +52,7 @@ path = ["src/test/resources/testdata/**/*.snbt"] SPDX-License-Identifier = "CC-BY-4.0" SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "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 <nea@nea.moe> +// +// 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 120000 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 100755 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/buildSrc/src/EnvFile.kt b/build-logic/src/main/kotlin/EnvFile.kt index ceec763..ceec763 100644 --- a/buildSrc/src/EnvFile.kt +++ b/build-logic/src/main/kotlin/EnvFile.kt 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/buildSrc/src/InnerJarsUnpacker.kt b/build-logic/src/main/kotlin/InnerJarsUnpacker.kt index de06467..de06467 100644 --- a/buildSrc/src/InnerJarsUnpacker.kt +++ b/build-logic/src/main/kotlin/InnerJarsUnpacker.kt diff --git a/buildSrc/src/RepoDownload.kt b/build-logic/src/main/kotlin/RepoDownload.kt index 42a09b3..42a09b3 100644 --- a/buildSrc/src/RepoDownload.kt +++ b/build-logic/src/main/kotlin/RepoDownload.kt 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<moe.nea.licenseextractificator.LicenseExtension> { + 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..7f55c5a --- /dev/null +++ b/build-logic/src/main/kotlin/firmament.repositories.gradle.kts @@ -0,0 +1,49 @@ +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" + } + maven("https://maven.parchmentmc.org") { + name = "ParchmentMC" + } + mavenLocal() +} diff --git a/buildSrc/src/licenseinfo.kt b/build-logic/src/main/kotlin/licenseinfo.kt index 50e4593..50e4593 100644 --- a/buildSrc/src/licenseinfo.kt +++ b/build-logic/src/main/kotlin/licenseinfo.kt diff --git a/buildSrc/src/lookupversion.kt b/build-logic/src/main/kotlin/lookupversion.kt index 8a7c2de..8a7c2de 100644 --- a/buildSrc/src/lookupversion.kt +++ b/build-logic/src/main/kotlin/lookupversion.kt diff --git a/build.gradle.kts b/build.gradle.kts index 1a31f16..9bb3e09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,15 +6,23 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import com.google.devtools.ksp.gradle.KspTaskJvm +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 net.fabricmc.loom.task.RemapJarTask +import net.fabricmc.loom.task.RemapSourcesJarTask +import net.fabricmc.loom.task.RunGameTask +import org.apache.tools.ant.taskdefs.condition.Os import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.plugin.SubpluginOption import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.utils.extendsFrom +import java.nio.charset.StandardCharsets +import java.util.* plugins { java @@ -23,16 +31,14 @@ plugins { alias(libs.plugins.kotlin.plugin.serialization) alias(libs.plugins.kotlin.plugin.powerassert) alias(libs.plugins.kotlin.plugin.ksp) - // alias(libs.plugins.loom) - // TODO: use arch loom once they update to 1.8 - id("fabric-loom") version "1.9.2" - alias(libs.plugins.shadow) - id("moe.nea.licenseextractificator") - id("moe.nea.mc-auto-translations") version "0.1.0" + alias(libs.plugins.loom) + alias(libs.plugins.shadow) apply false + id("firmament.common") + id("firmament.license-management") + alias(libs.plugins.mcAutoTranslations) } version = getGitTagInfo(libs.versions.minecraft.get()) -group = rootProject.property("maven_group").toString() java { withSourcesJar() @@ -40,6 +46,7 @@ java { languageVersion.set(JavaLanguageVersion.of(21)) } } + loom { mixin.useLegacyMixinAp.set(false) } @@ -50,54 +57,6 @@ tasks.withType(KotlinCompile::class) { } } -allprojects { - 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() - } -} kotlin { sourceSets.all { languageSettings { @@ -123,11 +82,21 @@ fun innerJarsOf(name: String, dependency: Dependency): Provider<FileTree> { 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<SourceSet> = mutableSetOf() -fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabled: Boolean = true): SourceSet { +fun createIsolatedSourceSet( + name: String, + path: String = "compat/$name", + isEnabled: Boolean = true, + inheritsFromMain: Boolean = true, + enableKsp: 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"))) @@ -135,8 +104,12 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl val mainSS = sourceSets.main.get() val upperName = ss.name.capitalizeN() afterEvaluate { - tasks.named("ksp${upperName}Kotlin", KspTaskJvm::class) { - this.options.add(SubpluginOption("apoption", "firmament.sourceset=${ss.name}")) + tasks.named("ksp${upperName}Kotlin", KspAATask::class) { + this.commandLineArgumentProviders.add { // TODO: update https://github.com/google/ksp/issues/2075 + listOf("firmament.sourceset=${ss.name}") + } + if (!enableKsp) + this.enabled = false } tasks.named("compile${upperName}Kotlin", KotlinCompile::class) { this.enabled = isEnabled @@ -153,23 +126,30 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl } configurations { (ss.implementationConfigurationName) { - extendsFrom(getByName(mainSS.compileClasspathConfigurationName)) + if (inheritsFromMain) + extendsFrom(getByName(mainSS.compileClasspathConfigurationName)) } (ss.annotationProcessorConfigurationName) { extendsFrom(getByName(mainSS.annotationProcessorConfigurationName)) } (mainSS.runtimeOnlyConfigurationName) { - extendsFrom(getByName(ss.runtimeClasspathConfigurationName)) - } - ("ksp$upperName") { - extendsFrom(ksp.get()) + if (isEnabled) + extendsFrom(getByName(ss.runtimeClasspathConfigurationName)) } + if (enableKsp) + ("ksp$upperName") { + extendsFrom(ksp.get()) + } } dependencies { - runtimeOnly(ss.output) - (ss.implementationConfigurationName)(sourceSets.main.get().output) + if (isEnabled) + runtimeOnly(ss.output) + if (inheritsFromMain) { + (ss.implementationConfigurationName)(project.files(tasks.compileKotlin.map { it.destinationDirectory })) + (ss.implementationConfigurationName)(project.files(tasks.compileJava.map { it.destinationDirectory })) + } } - tasks.shadowJar { + mergedSourceSetsJar.configure { from(ss.output) } // TODO: figure out why inheritances are not being respected by tiny kotlin names @@ -177,8 +157,7 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl classpath.from(configurations.getByName(ss.compileClasspathConfigurationName)) } collectTranslations { - // TODO: this does not work, somehow - this.classes.from(sourceSets.main.get().kotlin.classesDirectory) + this.classes.from(ss.kotlin.classesDirectory) } return ss } @@ -188,6 +167,11 @@ val SourceSet.modImplementationConfigurationName 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") @@ -214,24 +198,36 @@ val testAgent by configurations.creating { isVisible = false } +fabricApi.configureTests { + createSourceSet.set(true) + enableClientGameTests.set(true) + modId.set("firmament-gametest") + eula.set(true) + username.set("CoolGuy123") +} +val gameTestSourceSet by sourceSets.named("gametest") { + configurations.named(compileClasspathConfigurationName).extendsFrom(configurations.testCompileClasspath) + configurations.named(runtimeClasspathConfigurationName).extendsFrom(configurations.testRuntimeClasspath) +} -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 sodiumSourceSet = createIsolatedSourceSet("sodium") 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 reiSourceSet = createIsolatedSourceSet("rei", isEnabled = true) val moulconfigSourceSet = createIsolatedSourceSet("moulconfig") +val irisSourceSet = createIsolatedSourceSet("iris") val customTexturesSourceSet = createIsolatedSourceSet("texturePacks", "texturePacks") +val apiSourceSet = createIsolatedSourceSet("api", "api", inheritsFromMain = false, enableKsp = false) dependencies { // Minecraft dependencies "minecraft"(libs.minecraft) - "mappings"("net.fabricmc:yarn:${libs.versions.yarn.get()}:v2") + "mappings"(loom.layered { + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-1.21.10:2025.10.12@zip") + }) // Hotswap Dependency hotswap(libs.hotswap) @@ -242,21 +238,24 @@ dependencies { modImplementation(libs.moulconfig) modImplementation(libs.manninghamMills) modImplementation(libs.basicMath) + implementation(apiSourceSet.output) + (apiSourceSet.implementationConfigurationName)(loom.namedMinecraftJars) + (apiSourceSet.implementationConfigurationName)(libs.jspecify) + (apiSourceSet.implementationConfigurationName)(libs.jbAnnotations) + // configurations.forEach { println(it.name) } include(libs.basicMath) (modmenuSourceSet.modImplementationConfigurationName)(libs.modmenu) - (explosiveEnhancementSourceSet.modImplementationConfigurationName)(libs.explosiveenhancement) modImplementation(libs.hypixelmodapi) include(libs.hypixelmodapi.fabric) - compileOnly(project(":javaplugin")) - annotationProcessor(project(":javaplugin")) - implementation("com.google.auto.service:auto-service-annotations:1.1.1") + 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) - include(libs.moulconfig) - + shadowMe(libs.moulconfig) annotationProcessor(libs.mixinextras) - implementation(libs.mixinextras) + nonModImplentation(libs.mixinextras) include(libs.mixinextras) nonModImplentation(libs.nealisp) @@ -264,36 +263,25 @@ dependencies { modCompileOnly(libs.fabric.api) modRuntimeOnly(libs.fabric.api.deprecated) - modApi(libs.architectury) modCompileOnly(libs.jarvis.api) include(libs.jarvis.fabric) + (irisSourceSet.modImplementationConfigurationName)(libs.iris) (wildfireGenderSourceSet.modImplementationConfigurationName)(libs.femalegender) (wildfireGenderSourceSet.implementationConfigurationName)(customTexturesSourceSet.output) - (configuredSourceSet.modImplementationConfigurationName)(libs.configured) (sodiumSourceSet.modImplementationConfigurationName)(libs.sodium) + (sodiumSourceSet.implementationConfigurationName)(customTexturesSourceSet.output) + (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) { - exclude(module = "architectury") - exclude(module = "architectury-fabric") - } + + val reiDeps = libs.rei + (reiSourceSet.modImplementationConfigurationName)(reiDeps.api) + (reiSourceSet.modRuntimeOnlyConfigurationName)(reiDeps.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) @@ -306,26 +294,30 @@ dependencies { } - testImplementation("io.kotest:kotest-runner-junit5:6.0.0.M1") - testAgent(project(":testagent", configuration = "shadow")) + testImplementation("net.fabricmc:fabric-loader-junit:${libs.versions.fabric.loader.get()}") + testAgent(files(tasks.getByPath(":testagent:jar"))) + + "gametestImplementation"(sourceSets.test.map { it.output }) - implementation(project(":symbols")) - ksp(project(":symbols")) + implementation(projects.symbols) + ksp(projects.symbols) } loom { clientOnlyMinecraftJar() accessWidenerPath.set(project.file("src/main/resources/firmament.accesswidener")) runs { - removeIf { it.name != "client" } + removeIf { it.name == "server" } configureEach { property("fabric.log.level", "info") property("firmament.debug", "true") - property("firmament.classroots", - compatSourceSets.joinToString(File.pathSeparator) { - File(it.output.classesDirs.asPath).absolutePath - }) + 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) @@ -337,9 +329,9 @@ loom { named("client") { property("devauth.enabled", "true") vmArg("-ea") - vmArg("-XX:+AllowEnhancedClassRedefinition") - vmArg("-XX:HotswapAgent=external") - vmArg("-javaagent:${hotswap.resolve().single().absolutePath}") +// vmArg("-XX:+AllowEnhancedClassRedefinition") +// vmArg("-XX:HotswapAgent=external") +// vmArg("-javaagent:${hotswap.resolve().single().absolutePath}") } } } @@ -358,12 +350,16 @@ val updateTestRepo by tasks.registering { 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) + 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") + text = text.replace( + "firmament\\.compiletimerepohash=[^\n]*".toRegex(), + "firmament.compiletimerepohash=$latestSha" + ) propertiesFile.writeText(text) } } @@ -377,8 +373,10 @@ tasks.test { doFirst { wd.mkdirs() wd.resolve("config").deleteRecursively() - systemProperty("firmament.testrepo", - downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get()) + systemProperty( + "firmament.testrepo", + downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get() + ) jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}") } systemProperty("jdk.attach.allowAttachSelf", "true") @@ -396,44 +394,84 @@ tasks.withType<JavaCompile> { 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.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") } } +fun AbstractArchiveTask.badjar() = destinationDirectory.set(layout.buildDirectory.dir("badjars")) tasks.jar { - destinationDirectory.set(layout.buildDirectory.dir("badjars")) + badjar() archiveClassifier.set("slim") } +val apiJarNamed = tasks.register("apiJarNamed", Jar::class) { + badjar() + archiveClassifier = "api-named" + from(apiSourceSet.output) +} +val apiJar = tasks.register("apiJar", RemapJarTask::class) { + inputFile = apiJarNamed.flatMap { it.archiveFile } + addNestedDependencies = false + archiveClassifier = "api" +} +val apiSourcesJarNamed = tasks.register("apiSourcesJarNamed", Jar::class) { + badjar() + from(apiSourceSet.allSource) + archiveClassifier = "api-sources-named" +} +val apiSourcesJar = tasks.register("apiSourceSJar", RemapSourcesJarTask::class) { + inputFile = apiSourcesJarNamed.flatMap { it.archiveFile } + archiveClassifier = "api-sources" +} + +tasks.assemble { + dependsOn(apiJar) + dependsOn(apiSourcesJar) +} -tasks.shadowJar { +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("dev") + archiveClassifier.set("") relocate("io.github.moulberry.repo", "moe.nea.firmament.deps.repo") - destinationDirectory.set(layout.buildDirectory.dir("badjars")) + relocate("io.github.notenoughupdates.moulconfig", "moe.nea.firmament.deps.moulconfig") mergeServiceFiles() + transform<FabricModTransform>() } tasks.remapJar { // injectAccessWidener.set(true) - inputFile.set(tasks.shadowJar.flatMap { it.archiveFile }) - dependsOn(tasks.shadowJar) - archiveClassifier.set("") + 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) } @@ -445,17 +483,26 @@ tasks.processResources { from(collectTranslations) { into("assets/firmament/lang") } + from(project.files("translations/languages/")) { + 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.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).apply { +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 { @@ -463,10 +510,64 @@ tasks.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).ap } 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<AbstractArchiveTask>().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true } - -licensing.addExtraLicenseMatchers() diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 54719bc..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe> -// -// SPDX-License-Identifier: CC0-1.0 - -plugins { - kotlin("jvm") version "2.1.0" - `kotlin-dsl` -} -repositories { - mavenCentral() - maven { - name = "jitpack" - url = uri("https://jitpack.io") - } -} -dependencies { - implementation("com.github.romangraef:neaslicenseextractificator:1.1.0") - implementation("com.google.code.gson:gson:2.10.1") -} - -sourceSets { - main { - kotlin { - srcDir(file("src")) - } - } -} diff --git a/docs/firmament_logo.webp b/docs/firmament_logo.webp Binary files differnew file mode 100644 index 0000000..d70327a --- /dev/null +++ b/docs/firmament_logo.webp 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 Binary files differnew file mode 100644 index 0000000..2aba841 --- /dev/null +++ b/docs/firmament_logo_256.webp 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 Binary files differnew file mode 100644 index 0000000..c557fca --- /dev/null +++ b/docs/firmament_logo_256_nobg.webp 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 Binary files differnew file mode 100644 index 0000000..6e05b83 --- /dev/null +++ b/docs/firmament_logo_256_trans.webp 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 Binary files differnew file mode 100644 index 0000000..9b76f3c --- /dev/null +++ b/docs/firmament_logo_nobg.webp 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 Binary files differnew file mode 100644 index 0000000..73a15f7 --- /dev/null +++ b/docs/firmament_logo_trans.webp 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/generate-changelog.sh b/docs/generate-changelog.sh new file mode 100755 index 0000000..7509c9f --- /dev/null +++ b/docs/generate-changelog.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe> +# +# SPDX-License-Identifier: GPL-3.0-or-later +dbg() { + echo "$@" >&2 +} + +THIS_VERSION=$(git describe --tags --abbrev=0 HEAD|tr -d '\n') +LAST_VERSION=$(git describe --tags --abbrev=0 HEAD^|tr -d '\n') +echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$LAST_VERSION...$THIS_VERSION>" +git log --pretty='- %s ~%aN' --grep '[no changelog]' --invert-grep --fixed-strings "$LAST_VERSION..$THIS_VERSION" | sort diff --git a/docs/release_script.sh b/docs/release_script.sh index 43663b4..8d87a09 100755 --- a/docs/release_script.sh +++ b/docs/release_script.sh @@ -4,6 +4,8 @@ # 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([ @@ -23,20 +25,24 @@ die() begins_with_short_option() { - local first_option all_short_options='nh' + 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] [-h|--help]\n' "$0" + 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" } @@ -59,6 +65,30 @@ parse_commandline() { 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 @@ -129,19 +159,28 @@ fi echo "Confirming new version as $newversion" -echo Committing release commit -git commit --allow-empty -m 'Prepare release '"$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" + 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**: <https://github.com/nea89o/Firmament/compare/$oldversion...$newversion>" > "$releasenotes" +echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$oldversion...$comparetag>" > "$releasenotes" echo >> "$releasenotes" -git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$newversion" | tac >> "$releasenotes" +git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$comparetag" | tac >> "$releasenotes" echo >> "$releasenotes" echo Check Release notes: @@ -153,22 +192,29 @@ read echo Building JAR "$basedir"/gradlew --stop -"$basedir"/gradlew clean build +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 ---------------------------------------------- -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" +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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c9c1c1..292fc3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,76 +3,72 @@ # SPDX-License-Identifier: CC0-1.0 [versions] -minecraft = "1.21.4" +minecraft = "1.21.10" +# Update from https://maven.fabricmc.net/net/fabricmc/fabric-language-kotlin/ +fabric_kotlin = "1.13.7+kotlin.2.2.21" # Update from https://kotlinlang.org/ -kotlin = "2.1.0" +kotlin = "2.2.21" # Update from https://github.com/google/ksp/releases -kotlin_ksp = "2.1.0-1.0.29" - -# Update from https://linkie.shedaniel.me/dependencies?loader=fabric -fabric_loader = "0.16.9" -fabric_api = "0.114.0+1.21.4" -yarn = "1.21.4+build.4" -modmenu = "13.0.0-beta.1" -architectury = "15.0.1" -rei = "18.0.796" - -# Update from https://maven.fabricmc.net/net/fabricmc/fabric-language-kotlin/ -fabric_kotlin = "1.13.0+kotlin.2.1.0" +kotlin_ksp = "2.2.21-2.0.4" + +# Update from https://linkie.shedaniel.dev/dependencies?loader=fabric +yarn = "1.21.10+build.2" +fabric_loader = "0.18.0" +fabric_api = "0.138.3+1.21.10" +architectury = "18.0.6" +modmenu = "16.0.0-rc.1" +# Update from https://maven.architectury.dev/me/shedaniel/RoughlyEnoughItems-fabric/ (but is typically late) +rei = "21.9.812" +reidev = "33d8900cd6621816680634fcbae2dd07f1cffbd3" # 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" +loom = "1.13.457" # TODO: port back to architectury (and) 1.9.424 # Update from https://modrinth.com/mod/sodium/versions?l=fabric -sodium = "mc1.21.4-0.6.6-fabric" +sodium = "mc1.21.10-0.7.3-fabric" # Update from https://modrinth.com/mod/freecam/versions?l=fabric -freecammod = "1.3.2+mc1.21.4" - -# Update from https://modrinth.com/mod/no-chat-reports/versions?l=fabric -ncr = "Fabric-1.21.4-v2.11.0" +freecammod = "1.3.5+mc1.21.10" # Update from https://modrinth.com/mod/female-gender/versions?l=fabric -femalegender = "4.3.3+1.21.4" +femalegender = "5.0.0-Beta.2+1.21.10" -# Update from https://modrinth.com/mod/explosive-enhancement/versions?l=fabric -explosiveenhancement = "1.2.3-1.21.0" +# Update from https://modrinth.com/mod/iris/versions?l=fabric +iris = "1.9.6+1.21.10-fabric" + +shadow = "8.3.8" # Update from https://modrinth.com/mod/not-enough-animations/versions?l=fabric -notenoughanimations = "eZykTicT" +notenoughanimations = "rPkK5la7" -# 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 = "20.1.0+fabric" devauth = "1.2.1" -# Update from https://ktor.io/ -ktor = "3.0.3" - # Update from https://repo.nea.moe/#/releases/moe/nea/neurepoparser -neurepoparser = "1.6.0" +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" +mixinextras = "0.5.0" + +# Update from https://repo.nea.moe/#/releases/moe/nea/jarvis/ +jarvis = "2.1.0" -jarvis = "1.1.4" nealisp = "1.1.0" # Update from https://github.com/NotEnoughUpdates/MoulConfig/tags -moulconfig = "3.3.0" +moulconfig = "4.2.0-beta" -# Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20 -configured = "6023970" +# Update from https://repo.nea.moe/#/releases/moe/nea/mc-auto-translations/moe.nea.mc-auto-translations.gradle.plugin +mcAutoTranslations = "0.4.0" -# Update from https://modrinth.com/mod/hypixel-mod-api/versions +# 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" @@ -81,16 +77,16 @@ 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.2+1.21.4-fabric" +yacl = "3.8.0+1.21.9-fabric" -# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/0.6.1/ +# 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.0" +classtransform = "1.14.1" # Update from https://mvnrepository.com/artifact/org.ow2.asm/asm/ -asm = "9.7.1" +asm = "9.8" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } @@ -99,35 +95,40 @@ fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fab 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", version.ref = "moulconfig" } +rei_fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" } + +rei_dev_api = { module = "com.github.shedaniel.roughlyenoughitems:RoughlyEnoughItems-api", version.ref = "reidev" } +rei_dev_fabric = { module = "com.github.shedaniel.roughlyenoughitems:RoughlyEnoughItems-fabric", version.ref = "reidev" } + +jspecify = { module = "org.jspecify:jspecify", version = "1.0.0" } +jbAnnotations = { module = "org.jetbrains:annotations", version = "26.0.2" } + +moulconfig = { module = "org.notenoughupdates.moulconfig:modern-1.21.10", 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" } +iris = { module = "maven.modrinth:iris", version.ref = "iris" } +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" } @@ -135,7 +136,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" } [bundles] runtime_required = [ - "rei_fabric", + # "rei_fabric", # "notenoughanimations", "hypixelmodapi_fabric", ] @@ -143,9 +144,8 @@ runtime_optional = [ "devauth", # "freecammod", # "sodium", - # "qolify", - "ncr", - # "citresewn", + # "qolify", + # "ncr", ] [plugins] @@ -153,5 +153,6 @@ 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" } -shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +mcAutoTranslations = { id = "moe.nea.mc-auto-translations", version.ref = "mcAutoTranslations" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6e2ced2..9d0bbd2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,6 +4,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/javaplugin/build.gradle.kts b/javaplugin/build.gradle.kts index 6982a5a..dc461bc 100644 --- a/javaplugin/build.gradle.kts +++ b/javaplugin/build.gradle.kts @@ -1,6 +1,7 @@ plugins { java idea + id("firmament.common") } dependencies { implementation("net.fabricmc:stitch:0.6.2") diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java index cb87b20..d45563b 100644 --- a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java @@ -31,7 +31,7 @@ public class IntermediaryMethodReplacer extends TreeScanner<Void, Void> { public void replaceMethodName(JCTree.JCMethodInvocation node) { var select = node.getMethodSelect(); if (!(select instanceof JCTree.JCFieldAccess fieldAccess)) return; - if (!fieldAccess.name.contentEquals("methodName")) return; + if (!fieldAccess.name.contentEquals("intermediaryMethod")) 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; @@ -43,14 +43,17 @@ public class IntermediaryMethodReplacer extends TreeScanner<Void, Void> { type.tsym.flatName().toString(), clearName ); - fieldAccess.name = plugin.names.fromString("id"); - node.args = List.of(plugin.treeMaker.Literal(intermediaryName)); + fieldAccess.name = plugin.names.fromString("ofMethod"); + var args = List.<JCTree.JCExpression>of(plugin.treeMaker.Literal(intermediaryName.interMethodName())); + args.tail = List.of(plugin.treeMaker.Literal(intermediaryName.interClassName())); + args.tail.tail = node.args.tail; + node.args = args; } public void replaceClassName(JCTree.JCMethodInvocation node) { var select = node.getMethodSelect(); if (!(select instanceof JCTree.JCFieldAccess fieldAccess)) return; - if (!fieldAccess.name.contentEquals("className")) return; + if (!fieldAccess.name.contentEquals("intermediaryClass")) 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; @@ -63,7 +66,7 @@ public class IntermediaryMethodReplacer extends TreeScanner<Void, Void> { plugin.utils.reportError(sourceFile, node, "Unknown class name " + sourceName); return; } - fieldAccess.name = plugin.names.fromString("id"); + fieldAccess.name = plugin.names.fromString("ofIntermediaryClass"); node.typeargs = List.nil(); node.args = List.of(plugin.treeMaker.Literal(mappedName)); } diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java index eef5f9a..36713b6 100644 --- a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java @@ -18,15 +18,21 @@ public class MappingTree { 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)); + .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) { + public record MethodCoordinate( + String interClassName, + String interMethodName + ) { + } + + public MethodCoordinate resolveMethodToIntermediary(String className, String methodName) { var classData = classLookup.get(className.replace(".", "/")); TinyMethod candidate = null; for (TinyMethod method : classData.getMethods()) { @@ -37,7 +43,11 @@ public class MappingTree { candidate = method; } } - return candidate.getMethodNames().get(targetIndex); + if (candidate == null) + throw new RuntimeException("Couldd not find candidate for method " + className + "." + methodName); + return new MethodCoordinate( + classData.getClassNames().get(targetIndex), + candidate.getMethodNames().get(targetIndex)); } public String resolveClassToIntermediary(String className) { @@ -46,6 +56,6 @@ public class MappingTree { return null; } return cls.getClassNames().get(targetIndex) - .replace("/", "."); + .replace("/", "."); } } diff --git a/settings.gradle.kts b/settings.gradle.kts index aef468a..7b298b6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,8 @@ pluginManagement { rootProject.name = "Firmament" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include("symbols") include("javaplugin") include("testagent") +includeBuild("build-logic") @@ -1,42 +1,69 @@ # SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe> # # SPDX-License-Identifier: GPL-3.0-or-later -{pkgs ? import <nixpkgs> {}}: +{ + pkgs ? import <nixpkgs> { }, +}: pkgs.mkShell { - buildInputs = with pkgs; [ - bash - gh - git - xdg-utils - reuse - pre-commit - glfw - jdk21 - libGL - wayland - flite - jack2 - openal - pulseaudio - pipewire - glibc - ]; + buildInputs = + with pkgs; + [ + bash + gh + git + xdg-utils + reuse + pre-commit + glfw + jdk21 + libGL + wayland + flite + jack2 + openal + pulseaudio + pipewire + glibc + ] + ++ ([ + # For web + python3 + node-gyp + node-pre-gyp + nodejs_22 + corepack + pixman + cairo.dev + pango.dev + glib.dev + harfbuzz.dev + gnumake + fontconfig.dev + clang + freetype.dev + libpng.dev + pkg-config + ]); + LD_LIBRARY_PATH = pkgs.lib.strings.makeLibraryPath ( + with pkgs; + [ + glfw + pipewire + jack2 + pulseaudio + openal + libGL + wayland + flite + ] + ); + JAVA_HOME = "${pkgs.jdk21}"; 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/api/java/moe/nea/firmament/api/v1/FirmamentAPI.java b/src/api/java/moe/nea/firmament/api/v1/FirmamentAPI.java new file mode 100644 index 0000000..d01e88b --- /dev/null +++ b/src/api/java/moe/nea/firmament/api/v1/FirmamentAPI.java @@ -0,0 +1,45 @@ +package moe.nea.firmament.api.v1; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +/** + * Methods you can call to get information about firmaments current state. + */ +@ApiStatus.NonExtendable +public abstract class FirmamentAPI { + private static @Nullable FirmamentAPI INSTANCE; + + /** + * @return the canonical instance of the {@link FirmamentAPI}. + */ + public static FirmamentAPI getInstance() { + if (INSTANCE != null) + return INSTANCE; + try { + return INSTANCE = (FirmamentAPI) Class.forName("moe.nea.firmament.impl.v1.FirmamentAPIImpl") + .getField("INSTANCE") + .get(null); + } catch (IllegalAccessException | NoSuchFieldException | ClassCastException e) { + throw new RuntimeException("Firmament API implementation class found, but could not load api instance.", e); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not find Firmament API, check FabricLoader.getInstance().isModLoaded(\"firmament\") first."); + } + } + + /** + * @return list-view of registered extensions + */ + public abstract List<? extends FirmamentExtension> getExtensions(); + + /** + * Obtain a reference to the currently hovered item widget, which may be either in the item list or placed in a UI. + * This widget may or may not also be present in the Widgets on the current screen. + * + * @return the currently hovered firmament item widget. + */ + public abstract Optional<FirmamentItemWidget> getHoveredItemWidget(); +} diff --git a/src/api/java/moe/nea/firmament/api/v1/FirmamentExtension.java b/src/api/java/moe/nea/firmament/api/v1/FirmamentExtension.java new file mode 100644 index 0000000..89547cb --- /dev/null +++ b/src/api/java/moe/nea/firmament/api/v1/FirmamentExtension.java @@ -0,0 +1,43 @@ +package moe.nea.firmament.api.v1; + +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; + +import java.util.Collection; +import java.util.List; + +/** + * An extension to firmament, allowing you to hook into firmaments functions. + * + * <p>To register, set the entrypoint {@code firmament:v1} to an implementation of this class.</p> + * @see #ENTRYPOINT_NAME + */ +public interface FirmamentExtension { + + /** + * Name of the entry point that should be used registering firmament extensions. + */ + String ENTRYPOINT_NAME = "firmament:v1"; + + /** + * This method gets called during client initialization, if firmament is installed. Can be used as an alternative to + * checking {@code FabricLoader.getInstance().isModLoaded("firmament")}. + */ + default void onLoad() {} + + /** + * @param screen the current active screen + * @return whether inventory buttons should be hidden on the current screen. + */ + default boolean shouldHideInventoryButtons(Screen screen) { + return false; + } + + /** + * @param screen the current active screen + * @return a list of zones which contain content rendered by other mods, which should therefore hide the items in those areas + */ + default Collection<? extends ScreenRectangle> getExclusionZones(Screen screen) { + return List.of(); + } +} diff --git a/src/api/java/moe/nea/firmament/api/v1/FirmamentItemWidget.java b/src/api/java/moe/nea/firmament/api/v1/FirmamentItemWidget.java new file mode 100644 index 0000000..89bb17c --- /dev/null +++ b/src/api/java/moe/nea/firmament/api/v1/FirmamentItemWidget.java @@ -0,0 +1,37 @@ +package moe.nea.firmament.api.v1; + +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.NonExtendable +public interface FirmamentItemWidget { + /** + * non-exhaustive enum for the placement position of an item widget + */ + @ApiStatus.NonExtendable + interface Placement { + String name(); + + Placement ITEM_LIST = () -> "ITEM_LIST"; + Placement RECIPE_SCREEN = () -> "RECIPE_SCREEN"; + } + + /** + * @return where in the UI the item widget is placed + */ + Placement getPlacement(); + + /** + * get the currently displayed {@link ItemStack}. this empty stack might be empty. care should be taken not to + * mutate the item stack instance, without {@link ItemStack#copy copying} it first. + * + * @return the currently displayed {@link ItemStack}, may be empty. + */ + ItemStack getItemStack(); + + /** + * @return a SkyBlock id, potentially processed to reflect more details about the item stack, such as which specific + * {@code PET} it is. this is sometimes referred to as the "neu id". + */ + String getSkyBlockId(); +} diff --git a/src/api/java/moe/nea/firmament/api/v1/package-info.java b/src/api/java/moe/nea/firmament/api/v1/package-info.java new file mode 100644 index 0000000..62719b2 --- /dev/null +++ b/src/api/java/moe/nea/firmament/api/v1/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package moe.nea.firmament.api.v1; + +import org.jspecify.annotations.NullMarked; diff --git a/src/compat/citresewn/java/ConditionDirectAccessHelper.kt b/src/compat/citresewn/java/ConditionDirectAccessHelper.kt deleted file mode 100644 index af97a40..0000000 --- a/src/compat/citresewn/java/ConditionDirectAccessHelper.kt +++ /dev/null @@ -1,59 +0,0 @@ -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<StringMatcher> { - // 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<ConditionNBT, StringMatcher> { 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<StringMatcher, String> { 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<Any>() - - @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 deleted file mode 100644 index 0743d40..0000000 --- a/src/compat/citresewn/java/moe/nea/firmament/mixins/compat/citresewn/MixinConditionComponents.java +++ /dev/null @@ -1,123 +0,0 @@ -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<PropertyKey> keyRef, - @Local(argsOnly = true) LocalRef<PropertyValue> 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<DataResult> 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<Boolean> 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 deleted file mode 100644 index afe0375..0000000 --- a/src/compat/configured/java/BaseConfigNode.kt +++ /dev/null @@ -1,40 +0,0 @@ -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<IConfigEntry> { - 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 deleted file mode 100644 index 4e33b8b..0000000 --- a/src/compat/configured/java/ConfigCategory.kt +++ /dev/null @@ -1,48 +0,0 @@ -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<IModConfig>?) { - } - -} diff --git a/src/compat/configured/java/ConfigNode.kt b/src/compat/configured/java/ConfigNode.kt deleted file mode 100644 index 16e54a6..0000000 --- a/src/compat/configured/java/ConfigNode.kt +++ /dev/null @@ -1,39 +0,0 @@ -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<IConfigEntry> { - 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 deleted file mode 100644 index e16c51c..0000000 --- a/src/compat/configured/java/ConfigValue.kt +++ /dev/null @@ -1,72 +0,0 @@ -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<T: Any>(val option: ManagedOption<T>) : IConfigValue<T> { - 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 deleted file mode 100644 index df59739..0000000 --- a/src/compat/configured/java/ConfigValueNode.kt +++ /dev/null @@ -1,37 +0,0 @@ -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<IConfigEntry> { - 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 deleted file mode 100644 index 8e8b022..0000000 --- a/src/compat/configured/java/ConfiguredCompat.kt +++ /dev/null @@ -1,30 +0,0 @@ -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<IModConfig> { - 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 deleted file mode 100644 index c0095bf..0000000 --- a/src/compat/configured/java/ConfiguredConfigScreenProvider.kt +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index caf16a5..0000000 --- a/src/compat/explosiveEnhancement/java/ExplosiveEnhancementSpawner.kt +++ /dev/null @@ -1,17 +0,0 @@ -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/iris/java/moe/nea/firmament/compat/iris/Compat.kt b/src/compat/iris/java/moe/nea/firmament/compat/iris/Compat.kt new file mode 100644 index 0000000..6d04842 --- /dev/null +++ b/src/compat/iris/java/moe/nea/firmament/compat/iris/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.iris + +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("iris") + } +} diff --git a/src/compat/iris/java/moe/nea/firmament/compat/iris/IrisPipelineAssigner.kt b/src/compat/iris/java/moe/nea/firmament/compat/iris/IrisPipelineAssigner.kt new file mode 100644 index 0000000..b7b3e42 --- /dev/null +++ b/src/compat/iris/java/moe/nea/firmament/compat/iris/IrisPipelineAssigner.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.compat.iris + +import net.irisshaders.iris.api.v0.IrisApi +import net.irisshaders.iris.api.v0.IrisProgram +import util.render.CustomRenderPipelines +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientInitEvent + +object IrisPipelineAssigner { + @Subscribe + fun initIrisAssignments(event: ClientInitEvent) { + val api = IrisApi.getInstance() + api.assignPipeline(CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, IrisProgram.TEXTURED) + api.assignPipeline(CustomRenderPipelines.OMNIPRESENT_LINES, IrisProgram.LINES) + api.assignPipeline(CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS, IrisProgram.BASIC) + api.assignPipeline(CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS, IrisProgram.TEXTURED) + api.assignPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER, IrisProgram.ENTITIES) + } +} 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..58344ad --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt @@ -0,0 +1,39 @@ +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.world.phys.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] ?: return accessor + return registration.blockAccessor() + .from(accessor) + .serversideRep(customBlock.getDisplayItem(accessor.block)) + .build() + } + + companion object { + @JvmStatic + fun hasCustomBlock(accessor: BlockAccessor): Boolean { + return getCustomBlock(accessor) != null + } + + @JvmStatic + fun getCustomBlock(accessor: BlockAccessor): MiningRepoData.CustomMiningBlock? { + val item = accessor.serversideRep ?: return null + 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..01c06e5 --- /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.world.level.block.state.BlockState +import net.minecraft.resources.ResourceLocation +import net.minecraft.core.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(): ResourceLocation = + 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?.isDestroying ?: 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.immutable(), 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?.destroyBlockPos ?: 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..0083d78 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt @@ -0,0 +1,72 @@ +package moe.nea.firmament.compat.jade + +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.Element +import snownee.jade.api.ui.JadeUI +import snownee.jade.gui.JadeLinearLayout +import snownee.jade.impl.ui.ItemStackElement +import snownee.jade.impl.ui.TextElementImpl +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +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 TextElementImpl && it.width == 0 && it.string.isNotEmpty() + } + val canHarvest = SBItemStack(MC.stackInHand).breakingPower >= customBlock.breakingPower + val lastItem = innerMut[lastItemIndex] as ItemStackElement + if (harvestIndicator < 0) { + innerMut.add(lastItemIndex + 1, canHarvestIndicator(canHarvest)) + } else { + innerMut.set(harvestIndicator, canHarvestIndicator(canHarvest)) + } + innerMut.set( + lastItemIndex, JadeUI + .item(tool, 0.75f) + ) + innerMut.subList(0, lastItemIndex - 1).removeIf { it is ItemStackElement } + innerMut + } + }) + } + + fun canHarvestIndicator(canHarvest: Boolean): Element { + val t = IThemeHelper.get() + val text = if (canHarvest) t.success(CHECK) else t.danger(X) + return JadeUI.text(text) + .scale(0.75F) + .alignSelfCenter() + } + + private val CHECK: Component = Component.literal("✔") + private val X: Component = Component.literal("✕") + + override fun getUid(): ResourceLocation { + 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..a31492e --- /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.world.level.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..c289810 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt @@ -0,0 +1,52 @@ +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.world.level.block.Block +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object JadeIntegration { + @Config + object TConfig : ManagedConfig("jade-integration", Category.INTEGRATIONS) { + val miningProgress by toggle("progress") { true } + val blockDetection by toggle("blocks") { true } + } + + var customBlocks: Map<Block, MiningRepoData.CustomMiningBlock> = 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/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java new file mode 100644 index 0000000..5fabba8 --- /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.world.level.block.Blocks; +import net.minecraft.world.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/world/level/block/state/BlockState;requiresCorrectToolForDrops()Z")) + private boolean overwriteRequiresTool(boolean original, @Local(argsOnly = true) BlockAccessor accessor) { + if (CustomFakeBlockProvider.hasCustomBlock(accessor)) + return true; + return original; + } + + private static final List<ItemStack> 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<ItemStack>) 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..76084f8 --- /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.renderer.LevelRenderer; +import net.minecraft.core.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(LevelRenderer.class) +public class OnUpdateBreakProgress { + @Inject(method = "destroyBlockProgress", 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().destroyBlockPos, 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..898923a --- /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/multiplayer/MultiPlayerGameMode;destroyProgress:F", opcode = Opcodes.GETFIELD) + ) + private static float replaceBlockBreakingProgress(float original) { + return CustomMiningHardnessProvider.replaceBreakProgress(original); + } + + @ModifyExpressionValue(method = "drawBreakingProgress", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;getDestroyProgress(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/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 index b734e2c..ff58c20 100644 --- a/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt +++ b/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt @@ -6,6 +6,6 @@ import moe.nea.firmament.gui.config.AllConfigsGui class FirmamentModMenuPlugin : ModMenuApi { override fun getModConfigScreenFactory(): ConfigScreenFactory<*> { - return ConfigScreenFactory { AllConfigsGui.makeScreen(it) } + return ConfigScreenFactory { AllConfigsGui.makeScreen(parent = it) } } } diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt index dec2559..6b2df21 100644 --- a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt +++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt @@ -1,13 +1,16 @@ 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.common.text.StructuredText import io.github.notenoughupdates.moulconfig.gui.GuiComponent -import io.github.notenoughupdates.moulconfig.gui.GuiElementWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.GuiElementComponent import io.github.notenoughupdates.moulconfig.gui.GuiOptionEditor import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign import io.github.notenoughupdates.moulconfig.gui.MoulConfigEditor @@ -20,9 +23,12 @@ 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.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory import io.github.notenoughupdates.moulconfig.processor.ProcessedOption import java.lang.reflect.Type @@ -30,21 +36,24 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable +import net.minecraft.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.util.data.ManagedConfig import moe.nea.firmament.gui.config.ManagedOption import moe.nea.firmament.gui.config.StringHandler import moe.nea.firmament.gui.toMoulConfig @@ -96,29 +105,31 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { 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 + wrapComponent( + RowComponent( + AlignComponent( + TextComponent( + IMinecraft.INSTANCE.defaultFontRenderer, + { StructuredText.of(formatter(setter.get())) }, + 25, + TextComponent.TextAlignment.CENTER, false, false + ), + GetSetter.constant(HorizontalAlign.CENTER), + GetSetter.constant(VerticalAlign.CENTER) ), - GetSetter.constant(HorizontalAlign.CENTER), - GetSetter.constant(VerticalAlign.CENTER) - ), - SliderComponent( - mappedSetter, - fromT(minValue), - fromT(maxValue), - minStep, - 40 + SliderComponent( + mappedSetter, + fromT(minValue), + fromT(maxValue), + minStep, + 40 + ) ) - )) + ) } } - fun <T> helpRegisterChoice() where T : Enum<T>, T : StringIdentifiable { + fun <T> helpRegisterChoice() where T : Enum<T>, T : StringRepresentable { register(ChoiceHandler::class.java as Class<ChoiceHandler<T>>) { handler, option, categoryAccordionId, configObject -> object : ProcessedEditableOptionFirm<T>(option, categoryAccordionId, configObject) { override fun createEditor(): GuiOptionEditor { @@ -183,10 +194,30 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { } } } + register(ColourHandler::class.java) { handler, option, accordionId, configObject -> + object : ProcessedEditableOptionFirm<ChromaColour>(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<Unit>(option, categoryAccordionId, configObject) { override fun createEditor(): GuiOptionEditor { - return GuiOptionEditorButton(this, -1, "Click", configObject) + return GuiOptionEditorButton(this, -1, StructuredText.of("Click"), configObject) } override fun toT(any: Any?): Unit? { @@ -205,7 +236,7 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { register(HudMetaHandler::class.java) { handler, option, categoryAccordionId, configObject -> object : ProcessedEditableOptionFirm<HudMeta>(option, categoryAccordionId, configObject) { override fun createEditor(): GuiOptionEditor { - return GuiOptionEditorButton(this, -1, "Edit HUD", configObject) + return GuiOptionEditorButton(this, -1, StructuredText.of("Edit HUD"), configObject) } override fun fromT(t: HudMeta): Any { @@ -302,101 +333,111 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { } } - override fun open(parent: Screen?): Screen { - val configObject = object : Config() { - override fun saveNow() { - ManagedConfig.allManagedConfigs.getAll().forEach { it.save() } - } + val configObject = object : Config() { + override fun saveNow() { + ManagedConfig.allManagedConfigs.getAll().forEach { it.markDirty() } + } - override fun shouldAutoFocusSearchbar(): Boolean { - return true - } + override fun shouldAutoFocusSearchbar(): Boolean { + return true + } + + override fun getTitle(): StructuredText { + return StructuredText.of("Firmament ${Firmament.version.friendlyString}") + } - override fun getTitle(): String { - return "Firmament" + @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: ResourceLocation, link: String) = object : Social() { + override fun onClick() { + MC.openUrl(link) } - @Deprecated("Deprecated in java") - override fun executeRunnable(runnableId: Int) { - if (runnableId >= 0) - ErrorUtil.softError("Executed runnable $runnableId") + override fun getTooltip(): List<StructuredText> { + return listOf(StructuredText.of(name)) } - override fun getDescriptionBehaviour(option: ProcessedOption?): DescriptionRendereringBehaviour { - return DescriptionRendereringBehaviour.EXPAND_PANEL + override fun getIcon(): MyResourceLocation { + return identifier.toMoulConfig() } + } - fun mkSocial(name: String, identifier: Identifier, link: String) = object : Social() { - override fun onClick() { - Util.getOperatingSystem().open(URI(link)) + private val socials = listOf<Social>( + 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<Social> { + return socials + } + } + val categories = ManagedConfig.Category.entries.map { + val options = mutableListOf<ProcessedOptionFirm>() + 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 getTooltip(): List<String> { - return listOf(name) + override fun getName(): StructuredText { + return MoulConfigPlatform.wrap(config.labelText) } - override fun getIcon(): MyResourceLocation { - return identifier.toMoulConfig() + override fun getDescription(): StructuredText { + return StructuredText.of("Missing description") } - } - - private val socials = listOf<Social>( - 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<Social> { - return socials - } - } - val categories = ManagedConfig.Category.entries.map { - val options = mutableListOf<ProcessedOptionFirm>() - 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 get(): Any { + return Unit + } - override fun getType(): Type { - return Unit.javaClass - } + override fun getType(): Type { + return Unit.javaClass + } - override fun set(value: Any?): Boolean { - return false - } + 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) + 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) } + + return@map ProcessedCategoryFirm(it, options) + } + + override fun open(search: String?, parent: Screen?): Screen { val editor = MoulConfigEditor(ProcessedCategory.collect(categories), configObject) - return GuiElementWrapper(editor) // TODO : add parent support + if (search != null) + editor.search(search) + editor.setWide(AllConfigsGui.ConfigConfig.enableWideMC) + return MoulConfigScreenComponent(Component.empty(), GuiContext(GuiElementComponent(editor)), parent) // TODO : add parent support } } diff --git a/src/compat/moulconfig/java/ProcessedCategoryFirm.kt b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt index 5313441..7c99528 100644 --- a/src/compat/moulconfig/java/ProcessedCategoryFirm.kt +++ b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt @@ -1,9 +1,11 @@ package moe.nea.firmament.compat.moulconfig +import io.github.notenoughupdates.moulconfig.common.text.StructuredText import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory import io.github.notenoughupdates.moulconfig.processor.ProcessedOption -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.data.ManagedConfig class ProcessedCategoryFirm( val category: ManagedConfig.Category, @@ -21,12 +23,12 @@ class ProcessedCategoryFirm( return "FirmamentCategory.${category.name}" } - override fun getDisplayName(): String { - return category.labelText.string + override fun getDisplayName(): StructuredText { + return MoulConfigPlatform.wrap(category.labelText) } - override fun getDescription(): String { - return category.description.string + override fun getDescription(): StructuredText { + return MoulConfigPlatform.wrap(category.description) } override fun getIdentifier(): String { diff --git a/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt index f0e9aa4..e1ad217 100644 --- a/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt +++ b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt @@ -1,6 +1,8 @@ package moe.nea.firmament.compat.moulconfig import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.common.text.StructuredText +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform import moe.nea.firmament.gui.config.ManagedOption import moe.nea.firmament.util.ErrorUtil @@ -14,12 +16,12 @@ abstract class ProcessedEditableOptionFirm<T : Any>( return "FirmamentOption:${managedConfig.name}:${managedOption.propertyName}" } - override fun getName(): String { - return managedOption.labelText.string + override fun getName(): StructuredText { + return MoulConfigPlatform.wrap(managedOption.labelText) } - override fun getDescription(): String { - return managedOption.labelDescription.string + override fun getDescription(): StructuredText { + return MoulConfigPlatform.wrap(managedOption.labelDescription) } abstract fun fromT(t: T): Any @@ -34,11 +36,11 @@ abstract class ProcessedEditableOptionFirm<T : Any>( ErrorUtil.softError("Failed to set value p0 in $this") return false } - managedConfig.save() + managedConfig.markDirty() return true } override fun explicitNotifyChange() { - managedConfig.save() + managedConfig.markDirty() } } diff --git a/src/compat/moulconfig/java/ProcessedOptionFirm.kt b/src/compat/moulconfig/java/ProcessedOptionFirm.kt index 4d0096c..6936048 100644 --- a/src/compat/moulconfig/java/ProcessedOptionFirm.kt +++ b/src/compat/moulconfig/java/ProcessedOptionFirm.kt @@ -10,6 +10,9 @@ 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 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 index 1097654..cc55fb8 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt @@ -5,9 +5,9 @@ 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 net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.world.entity.LivingEntity import moe.nea.firmament.gui.entity.EntityRenderer import moe.nea.firmament.util.ErrorUtil @@ -17,13 +17,13 @@ class EntityWidget( val point: Point, val size: FloatingDimension = FloatingDimension(defaultSize) ) : WidgetWithBounds() { - override fun children(): List<Element> { + override fun children(): List<GuiEventListener> { return emptyList() } var hasErrored = false - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { try { if (!hasErrored) { EntityRenderer.renderEntity( 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 index 98ac276..71e867a 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiCommonPlugin.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiCommonPlugin.kt @@ -2,9 +2,11 @@ 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 index 92f2cfc..ad0da89 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt @@ -1,5 +1,6 @@ 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 @@ -12,24 +13,29 @@ 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.SBCraftingRecipe -import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe -import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.gui.screens.inventory.MenuAccess +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.world.InteractionResult +import net.minecraft.resources.ResourceLocation +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 @@ -40,30 +46,40 @@ import moe.nea.firmament.util.unformattedString class FirmamentReiPlugin : REIClientPlugin { companion object { + @ExpensiveItemCacheApi fun EntryStack<SBItemStack>.asItemEntry(): EntryStack<ItemStack> { return EntryStack.of(VanillaEntryTypes.ITEM, value.asImmutableItemStack()) } - val SKYBLOCK_ITEM_TYPE_ID = Identifier.of("firmament", "skyblockitems") + val SKYBLOCK_ITEM_TYPE_ID = ResourceLocation.fromNamespaceAndPath("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 SBCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() - val neuItem = RepoManager.getNEUItem(SkyblockId(display.neuRecipe.output.itemId)) - ?: error("Could not find neu item ${display.neuRecipe.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")) + 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.TConfig.alwaysSuperCraft + if (neuItem.isVanilla && useSuperCraft) return@TransferHandler TransferHandler.Result.createFailed( + Component.translatable( + "firmament.recipe.novanilla" + ) + ) var shouldReturn = true if (context.isActuallyCrafting && !useSuperCraft) { - if (screen !is GenericContainerScreen || screen.title?.unformattedString != CraftingOverlay.CRAFTING_SCREEN_NAME) { + val craftingScreen = (screen as? ContainerScreen) + ?.takeIf { it.title?.string == CraftingOverlay.CRAFTING_SCREEN_NAME } + if (craftingScreen == null) { MC.sendCommand("craft") shouldReturn = false } - CraftingOverlay.setOverlay(screen as? GenericContainerScreen, display.neuRecipe) + CraftingOverlay.setOverlay(craftingScreen, recipe) } if (context.isActuallyCrafting && useSuperCraft) { shouldReturn = false @@ -74,50 +90,61 @@ class FirmamentReiPlugin : REIClientPlugin { } + val generics = listOf<GenericREIRecipeCategory<*>>( + // 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) { - registry.add(SBCraftingRecipe.Category) - registry.add(SBForgeRecipe.Category) + if (!RepoManager.shouldLoadREI()) return + + registry.add(generics) registry.add(SBMobDropRecipe.Category) registry.add(SBKatRecipe.Category) registry.add(SBReforgeRecipe.Category) - registry.add(SBEssenceUpgradeRecipe.Category) + registry.add(SBShopRecipe.Category) } override fun registerExclusionZones(zones: ExclusionZones) { - zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles } + zones.register(AbstractContainerScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles } zones.register(StorageOverlayScreen::class.java) { it.getBounds() } } override fun registerDisplays(registry: DisplayRegistry) { - registry.registerDisplayGenerator( - SBCraftingRecipe.Category.catIdentifier, - SkyblockCraftingRecipeDynamicGenerator) + if (!RepoManager.shouldLoadREI()) return + + generics.forEach { + it.registerDynamicGenerator(registry) + } registry.registerDisplayGenerator( SBReforgeRecipe.catIdentifier, SBReforgeRecipe.DynamicGenerator ) registry.registerDisplayGenerator( - SBForgeRecipe.Category.categoryIdentifier, - SkyblockForgeRecipeDynamicGenerator) - registry.registerDisplayGenerator( SBMobDropRecipe.Category.categoryIdentifier, - SkyblockMobDropRecipeDynamicGenerator) + SkyblockMobDropRecipeDynamicGenerator + ) registry.registerDisplayGenerator( - SBKatRecipe.Category.categoryIdentifier, - SkyblockKatRecipeDynamicGenerator) + SBShopRecipe.Category.categoryIdentifier, + SkyblockShopRecipeDynamicGenerator + ) registry.registerDisplayGenerator( - SBEssenceUpgradeRecipe.Category.categoryIdentifier, - SkyblockEssenceRecipeDynamicGenerator + SBKatRecipe.Category.categoryIdentifier, + SkyblockKatRecipeDynamicGenerator ) } override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) { - if (!RepoManager.Config.disableItemGroups) + if (!RepoManager.shouldLoadREI()) return + + if (!RepoManager.TConfig.disableItemGroups) RepoManager.neuRepo.constants.parents.parents .forEach { (parent, children) -> registry.group( SkyblockId(parent).identifier, - Text.literal(RepoManager.getNEUItem(SkyblockId(parent))?.displayName ?: parent), + Component.literal(RepoManager.getNEUItem(SkyblockId(parent))?.displayName ?: parent), (children + parent).map { SBItemEntryDefinition.getEntry(SkyblockId(it)) }) } } @@ -128,14 +155,16 @@ class FirmamentReiPlugin : REIClientPlugin { return screen == StorageOverlayScreen::class.java } - override fun <R : Screen?> shouldScreenBeOverlaid(screen: R): ActionResult { - return ActionResult.SUCCESS + override fun <R : Screen?> shouldScreenBeOverlaid(screen: R): InteractionResult { + return InteractionResult.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 index b917c3e..a19355b 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/HoveredItemStackProvider.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/HoveredItemStackProvider.kt @@ -5,17 +5,18 @@ 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 net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.components.events.ContainerEventHandler +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.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? { + override fun provideHoveredItemStack(screen: Screen): ItemStack? { val entryStack = ScreenRegistry.getInstance().getFocusedStack(screen, PointHelper.ofMouse()) ?: return null return entryStack.value as? ItemStack ?: entryStack.cheatsAs().value @@ -25,14 +26,14 @@ class ScreenRegistryHoveredItemStackProvider : HoveredItemStackProvider { @AutoService(HoveredItemStackProvider::class) @CompatLoader.RequireMod("roughlyenoughitems") class OverlayHoveredItemStackProvider : HoveredItemStackProvider { - override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? { - var baseElement: Element? = REIRuntime.getInstance().overlay.orElse(null) + override fun provideHoveredItemStack(screen: Screen): ItemStack? { + var baseElement: GuiEventListener? = 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) + if (baseElement !is ContainerEventHandler) return null + baseElement = baseElement.getChildAt(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 index a7b4c99..0e24f4e 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt @@ -15,41 +15,74 @@ 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.tooltip.TooltipType -import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.item.TooltipFlag +import net.minecraft.network.chat.Component 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.MC +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<SBItemStack> { + @OptIn(ExpensiveItemCacheApi::class) override fun render( entry: EntryStack<SBItemStack>, - context: DrawContext, + context: GuiGraphics, bounds: Rectangle, mouseX: Int, mouseY: Int, delta: Float ) { - 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( - entry.asItemEntry().value, - -8, -8, + val neuItem = entry.value.neuItem + val itemToRender = if(!RepoManager.TConfig.perfectRenders.rendersPerfectVisuals() && !entry.value.isWarm() && neuItem != null) { + ItemCache.recacheSoon(neuItem) + ItemStack(Items.PAINTING) + } else { + entry.value.asImmutableItemStack() + } + + context.pose().pushMatrix() + context.pose().translate(bounds.centerX.toFloat(), bounds.centerY.toFloat()) + context.pose().scale(bounds.width.toFloat() / 16F, bounds.height.toFloat() / 16F) + context.renderItem(itemToRender, -8, -8) + context.renderItemDecorations( + MC.font, itemToRender, -8, -8, + if (entry.value.getStackSize() > 1000) FirmFormatters.shortFormat( + entry.value.getStackSize() + .toDouble() + ) + else null ) - context.matrices.pop() + context.pose().popMatrix() } - val minecraft = MinecraftClient.getInstance() - var canUseVanillaTooltipEvents = false + val minecraft = Minecraft.getInstance() + var canUseVanillaTooltipEvents = true + @OptIn(ExpensiveItemCacheApi::class) override fun getTooltip(entry: EntryStack<SBItemStack>, tooltipContext: TooltipContext): Tooltip? { + if (!entry.value.isWarm() && !RepoManager.TConfig.perfectRenders.rendersPerfectText()) { + val neuItem = entry.value.neuItem + if (neuItem != null) { + val lore = mutableListOf<Component>() + lore.add(Component.literal(neuItem.displayName)) + neuItem.lore.mapTo(mutableListOf()) { Component.literal(it) } + return Tooltip.create(lore) + } + } + val stack = entry.value.asImmutableItemStack() val lore = mutableListOf(stack.displayNameAccordingToNbt) @@ -57,19 +90,24 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> { if (canUseVanillaTooltipEvents) { try { ItemTooltipCallback.EVENT.invoker().getTooltip( - stack, tooltipContext.vanillaContext(), TooltipType.BASIC, lore + stack, tooltipContext.vanillaContext(), TooltipFlag.Default.NORMAL, lore ) } catch (ex: Exception) { + canUseVanillaTooltipEvents = false ErrorUtil.softError("Failed to use vanilla tooltips", ex) } } else { - ItemTooltipEvent.publish(ItemTooltipEvent( - stack, - tooltipContext.vanillaContext(), - TooltipType.BASIC, - lore - )) + ItemTooltipEvent.publish( + ItemTooltipEvent( + stack, + tooltipContext.vanillaContext(), + TooltipFlag.Default.NORMAL, + lore + ) + ) } + if (entry.value.getStackSize() > 1000 && lore.isNotEmpty()) + lore.add(1, Component.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( @@ -77,7 +115,7 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> { // ?: MC.defaultRegistries // ), // MC.player, -// TooltipType.BASIC +// TooltipFlag.Default.NORMAL // ) 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 index 724d193..9f32472 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntrySerializer.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntrySerializer.kt @@ -2,8 +2,8 @@ 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 net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec import moe.nea.firmament.repo.SBItemStack object NEUItemEntrySerializer : EntrySerializer<SBItemStack> { @@ -11,7 +11,7 @@ object NEUItemEntrySerializer : EntrySerializer<SBItemStack> { return SBItemStack.CODEC } - override fun streamCodec(): PacketCodec<RegistryByteBuf, SBItemStack> { + override fun streamCodec(): StreamCodec<RegistryFriendlyByteBuf, SBItemStack> { 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..54bc7f0 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt @@ -0,0 +1,82 @@ +package moe.nea.firmament.compat.rei + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +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.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.LivingEntity +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<Widget> = mutableListOf() + fun <T: Widget> add(t: T): T = t.also(container::add) + + override fun createCyclingItemSlot( + x: Int, + y: Int, + content: List<SBItemStack>, + slotKind: RecipeLayouter.SlotKind + ): RecipeLayouter.CyclingItemSlot { + val slot = Widgets.createSlot(Point(x, y)) + if (content.isNotEmpty()) + slot.entries(content.map { SBItemEntryDefinition.getEntry(it) }) + 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))) + } + RecipeLayouter.SlotKind.DISPLAY -> { + slot.disableBackground() + slot.disableHighlight() + } + } + add(slot) + return object : RecipeLayouter.CyclingItemSlot { + override fun current(): SBItemStack = content.firstOrNull() ?: SBItemStack.EMPTY + override fun update(newValue: SBItemStack) {} + override fun onUpdate(action: () -> Unit) {} + } + } + + override fun createTooltip(rectangle: Rectangle, label: List<Component>) { + add(Widgets.createTooltip(rectangle, *label.toTypedArray())) + } + + override fun createLabel(x: Int, y: Int, text: Component): RecipeLayouter.Updater<Component> { + val label = add(Widgets.createLabel(Point(x, y), text)) + return object : RecipeLayouter.Updater<Component> { + override fun update(newValue: Component) { + label.message = newValue + } + } + } + + 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(point: Point, animationTicks: Int) { + add(Widgets.createBurningFire(point).animationDurationTicks(animationTicks.toDouble())) + } + + override fun createEntity(rectangle: Rectangle, entity: LivingEntity) { + add(EntityWidget(entity, rectangle.location, FloatingDimension(rectangle.size))) + } +} 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 index 9638281..fde3e3d 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt @@ -9,24 +9,23 @@ 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 net.minecraft.world.level.ItemLike +import net.minecraft.world.item.ItemStack +import net.minecraft.tags.TagKey +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry -import moe.nea.firmament.repo.PetData +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.petData -import moe.nea.firmament.util.skyBlockId object SBItemEntryDefinition : EntryDefinition<SBItemStack> { 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<SBItemStack>?, value: SBItemStack): ItemStack { return value.asCopiedItemStack() } @@ -44,8 +43,14 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { return Stream.empty() } - override fun asFormattedText(entry: EntryStack<SBItemStack>, value: SBItemStack): Text { - return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack()) + @OptIn(ExpensiveItemCacheApi::class) + override fun asFormattedText(entry: EntryStack<SBItemStack>, value: SBItemStack): Component { + val neuItem = entry.value.neuItem + return if (!RepoManager.TConfig.perfectRenders.rendersPerfectText() || entry.value.isWarm() || neuItem == null) { + VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack()) + } else { + Component.literal(neuItem.displayName) + } } override fun hash(entry: EntryStack<SBItemStack>, value: SBItemStack, context: ComparisonContext): Long { @@ -54,8 +59,10 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { } override fun wildcard(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack { - return value.copy(stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId), - stars = 0, extraLore = listOf()) + return value.copy( + stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId), + stars = 0, extraLore = listOf(), reforge = null + ) } override fun normalize(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack { @@ -70,7 +77,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { return value.getStackSize() == 0 } - override fun getIdentifier(entry: EntryStack<SBItemStack>?, value: SBItemStack): Identifier { + override fun getIdentifier(entry: EntryStack<SBItemStack>?, value: SBItemStack): ResourceLocation { return value.skyblockId.identifier } @@ -83,15 +90,8 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> = getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt()) - fun getPassthrough(item: ItemConvertible) = getEntry(SBItemStack.passthrough(ItemStack(item.asItem()))) + fun getPassthrough(item: ItemLike) = getEntry(SBItemStack.passthrough(ItemStack(item.asItem()))) fun getEntry(stack: ItemStack): EntryStack<SBItemStack> = - getEntry( - SBItemStack( - stack.skyBlockId ?: SkyblockId.NULL, - RepoManager.getNEUItem(stack.skyBlockId ?: SkyblockId.NULL), - stack.count, - petData = stack.petData?.let { PetData.fromHypixel(it) } - ) - ) + 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 index f52f418..900ebab 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt @@ -1,65 +1,56 @@ - - package moe.nea.firmament.compat.rei -import io.github.moulberry.repo.data.NEUCraftingRecipe 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.SBCraftingRecipe -import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe -import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe 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 SkyblockCraftingRecipeDynamicGenerator = - neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> { SBCraftingRecipe(it) } - -val SkyblockForgeRecipeDynamicGenerator = - neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> { SBForgeRecipe(it) } - val SkyblockMobDropRecipeDynamicGenerator = - neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) } - + neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) } +val SkyblockShopRecipeDynamicGenerator = + neuDisplayGenerator<SBShopRecipe, NEUNpcShopRecipe> { SBShopRecipe(it) } val SkyblockKatRecipeDynamicGenerator = - neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) } -val SkyblockEssenceRecipeDynamicGenerator = - neuDisplayGenerator<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { SBEssenceUpgradeRecipe(it) } + neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) } inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(crossinline mapper: (T) -> D) = - object : DynamicDisplayGenerator<D> { - override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> { - if (entry.type != SBItemEntryDefinition.type) return Optional.empty() - val item = entry.castValue<SBItemStack>() - val recipes = RepoManager.getRecipesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() - return Optional.of(craftingRecipes.map(mapper)) - } - - override fun generate(builder: ViewSearchBuilder): Optional<List<D>> { - if (SBCraftingRecipe.Category.catIdentifier !in builder.categories) return Optional.empty() - return Optional.of( - RepoManager.getAllRecipes().filterIsInstance<T>().map { mapper(it) } - .toList() - ) - } - - override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> { - if (entry.type != SBItemEntryDefinition.type) return Optional.empty() - val item = entry.castValue<SBItemStack>() - val recipes = RepoManager.getUsagesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() - return Optional.of(craftingRecipes.map(mapper)) - - } - } + neuDisplayGeneratorWithItem<D, T> { _, it -> mapper(it) } + +inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(crossinline mapper: (SBItemStack, T) -> D) = + neuDisplayGeneratorWithItem(T::class.java, mapper) +inline fun <D : Display, T : NEURecipe> neuDisplayGeneratorWithItem( + filter: Class<T>, + crossinline mapper: (SBItemStack, T) -> D) = + object : DynamicDisplayGenerator<D> { + override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue<SBItemStack>() + val recipes = RepoManager.getRecipesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance<T>(filter) + return Optional.of(craftingRecipes.map { mapper(item, it) }) + } + + override fun generate(builder: ViewSearchBuilder): Optional<List<D>> { + return Optional.empty() // TODO: allows searching without blocking getRecipeFor + } + + override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue<SBItemStack>() + val recipes = RepoManager.getUsagesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance<T>(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 index cfb6f74..1db0376 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt @@ -6,19 +6,17 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen import moe.nea.firmament.mixins.accessor.AccessorHandledScreen -import moe.nea.firmament.util.skyBlockId object SkyblockItemIdFocusedStackProvider : FocusedStackProvider { override fun provide(screen: Screen?, mouse: Point?): CompoundEventResult<EntryStack<*>> { - if (screen !is HandledScreen<*>) return CompoundEventResult.pass() - screen as AccessorHandledScreen + if (screen !is AbstractContainerScreen<*>) return CompoundEventResult.pass() + if (screen !is AccessorHandledScreen) return CompoundEventResult.pass() val focusedSlot = screen.focusedSlot_Firmament ?: return CompoundEventResult.pass() - val item = focusedSlot.stack ?: return CompoundEventResult.pass() - val skyblockId = item.skyBlockId ?: return CompoundEventResult.pass() - return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(skyblockId)) + val item = focusedSlot.item ?: 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/recipes/GenericREIRecipeCategory.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt new file mode 100644 index 0000000..aefb37d --- /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.network.chat.Component +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<T : NEURecipe>( + val renderer: GenericRecipeRenderer<T>, +) : DisplayCategory<GenericRecipe<T>> { + private val dynamicGenerator = + neuDisplayGeneratorWithItem<GenericRecipe<T>, T>(renderer.typ) { item, recipe -> + GenericRecipe( + recipe, + item, + categoryIdentifier + ) + } + + private val categoryIdentifier = CategoryIdentifier.of<GenericRecipe<T>>(renderer.identifier) + override fun getCategoryIdentifier(): CategoryIdentifier<GenericRecipe<T>> { + return categoryIdentifier + } + + override fun getDisplayHeight(): Int { + return renderer.displayHeight + } + + override fun getTitle(): Component? { + return renderer.title + } + + override fun getIcon(): Renderer? { + return EntryStacks.of(renderer.icon) + } + + override fun setupDisplay(display: GenericRecipe<T>, bounds: Rectangle): List<Widget> { + 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<T : NEURecipe>( + override val neuRecipe: T, + val sourceItem: SBItemStack?, + val id: CategoryIdentifier<GenericRecipe<T>> +) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*>? { + return id + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt deleted file mode 100644 index 8db3d75..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt +++ /dev/null @@ -1,56 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -import io.github.moulberry.repo.data.NEUCraftingRecipe -import io.github.moulberry.repo.data.NEUIngredient -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.display.Display -import me.shedaniel.rei.api.common.display.DisplaySerializer -import me.shedaniel.rei.api.common.util.EntryStacks -import net.minecraft.block.Blocks -import net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.repo.SBItemStack - -class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() { - override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier - - object Category : DisplayCategory<SBCraftingRecipe> { - val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafing_recipe") - override fun getCategoryIdentifier(): CategoryIdentifier<out SBCraftingRecipe> = catIdentifier - - override fun getTitle(): Text = Text.literal("SkyBlock Crafting") - - override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.CRAFTING_TABLE) - override fun setupDisplay(display: SBCraftingRecipe, bounds: Rectangle): List<Widget> { - val point = Point(bounds.centerX - 58, bounds.centerY - 27) - return buildList { - add(Widgets.createRecipeBase(bounds)) - add(Widgets.createArrow(Point(point.x + 60, point.y + 18))) - add(Widgets.createResultSlotBackground(Point(point.x + 95, point.y + 19))) - for (i in 0 until 3) { - for (j in 0 until 3) { - val slot = Widgets.createSlot(Point(point.x + 1 + i * 18, point.y + 1 + j * 18)).markInput() - add(slot) - val item = display.neuRecipe.inputs[i + j * 3] - if (item == NEUIngredient.SENTINEL_EMPTY) continue - slot.entry(SBItemEntryDefinition.getEntry(item)) - } - } - add( - Widgets.createSlot(Point(point.x + 95, point.y + 19)) - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.output)) - .disableBackground().markOutput() - ) - } - } - - } - -} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt deleted file mode 100644 index ec71ec8..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt +++ /dev/null @@ -1,61 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -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 net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.repo.EssenceRecipeProvider -import moe.nea.firmament.repo.SBItemStack -import moe.nea.firmament.util.SkyblockId - -class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.EssenceUpgradeRecipe) : SBRecipe() { - object Category : DisplayCategory<SBEssenceUpgradeRecipe> { - override fun getCategoryIdentifier(): CategoryIdentifier<SBEssenceUpgradeRecipe> = - CategoryIdentifier.of(Firmament.MOD_ID, "essence_upgrade") - - override fun getTitle(): Text { - return Text.literal("Essence Upgrades") - } - - override fun getIcon(): Renderer { - return SBItemEntryDefinition.getEntry(SkyblockId("ESSENCE_WITHER")) - } - - override fun setupDisplay(display: SBEssenceUpgradeRecipe, bounds: Rectangle): List<Widget> { - val recipe = display.neuRecipe - val list = mutableListOf<Widget>() - list.add(Widgets.createRecipeBase(bounds)) - list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 - 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter - 1)))) - list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 + 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(recipe.essenceIngredient))) - list.add(Widgets.createSlot(Point(bounds.maxX - 12 - 16, bounds.centerY - 8)) - .markOutput() - .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter)))) - val extraItems = recipe.extraItems - list.add(Widgets.createArrow(Point(bounds.centerX - 24 / 2, - if (extraItems.isEmpty()) bounds.centerY - 17 / 2 - else bounds.centerY + 18 / 2))) - for ((index, item) in extraItems.withIndex()) { - list.add(Widgets.createSlot( - Point(bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18, - bounds.centerY - 18 / 2)) - .markInput() - .entry(SBItemEntryDefinition.getEntry(item))) - } - return list - } - } - - override fun getCategoryIdentifier(): CategoryIdentifier<*> { - return Category.categoryIdentifier - } -} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt deleted file mode 100644 index 92b2f3f..0000000 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt +++ /dev/null @@ -1,71 +0,0 @@ -package moe.nea.firmament.compat.rei.recipes - -import io.github.moulberry.repo.data.NEUForgeRecipe -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 kotlin.math.cos -import kotlin.math.sin -import kotlin.time.Duration.Companion.seconds -import net.minecraft.block.Blocks -import net.minecraft.text.Text -import moe.nea.firmament.Firmament -import moe.nea.firmament.compat.rei.SBItemEntryDefinition -import moe.nea.firmament.compat.rei.plus - -class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() { - override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier - - object Category : DisplayCategory<SBForgeRecipe> { - override fun getCategoryIdentifier(): CategoryIdentifier<SBForgeRecipe> = - CategoryIdentifier.of(Firmament.MOD_ID, "forge_recipe") - - override fun getTitle(): Text = Text.literal("Forge Recipes") - override fun getDisplayHeight(): Int { - return 104 - } - - override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.ANVIL) - override fun setupDisplay(display: SBForgeRecipe, bounds: Rectangle): List<Widget> { - return buildList { - add(Widgets.createRecipeBase(bounds)) - add(Widgets.createResultSlotBackground(Point(bounds.minX + 124, bounds.minY + 46))) - val arrow = Widgets.createArrow(Point(bounds.minX + 90, bounds.minY + 54 - 18 / 2)) - add(arrow) - add(Widgets.createTooltip(arrow.bounds, - Text.stringifiedTranslatable("firmament.recipe.forge.time", - display.neuRecipe.duration.seconds))) - val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) - val count = display.neuRecipe.inputs.size - if (count == 1) { - add( - Widgets.createSlot(Point(ingredientsCenter.x, ingredientsCenter.y)).markInput() - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.inputs.single())) - ) - } else { - display.neuRecipe.inputs.forEachIndexed { idx, ingredient -> - val rad = Math.PI * 2 * idx / count - add( - Widgets.createSlot( - Point( - cos(rad) * 30, - sin(rad) * 30, - ) + ingredientsCenter - ).markInput().entry(SBItemEntryDefinition.getEntry(ingredient)) - ) - } - } - add( - Widgets.createSlot(Point(bounds.minX + 124, bounds.minY + 46)).markOutput().disableBackground() - .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.outputStack)) - ) - } - } - } - -} 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 index cce1465..2ce27a1 100644 --- 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 @@ -8,7 +8,7 @@ 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import me.shedaniel.rei.api.client.gui.Renderer @@ -17,12 +17,13 @@ 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 net.minecraft.client.gui.navigation.ScreenDirection +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.world.item.Items +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.compat.rei.SBItemEntryDefinition import moe.nea.firmament.repo.PetData @@ -38,7 +39,7 @@ class SBKatRecipe(override val neuRecipe: NEUKatUpgradeRecipe) : SBRecipe() { override fun getCategoryIdentifier(): CategoryIdentifier<SBKatRecipe> = CategoryIdentifier.of(Firmament.MOD_ID, "kat_recipe") - override fun getTitle(): Text = Text.literal("Kat Pet Upgrade") + override fun getTitle(): Component = Component.literal("Kat Pet Upgrade") override fun getDisplayHeight(): Int { return 100 } @@ -55,11 +56,13 @@ class SBKatRecipe(override val neuRecipe: NEUKatUpgradeRecipe) : SBRecipe() { val inputLevelLabelCenter = Point(bounds.minX + 30 - 18 + 5 + 8, bounds.minY + 25) val inputLevelLabel = Widgets.createLabel( inputLevelLabelCenter, - Text.literal("")).centered() + Component.literal("") + ).centered() val outputLevelLabelCenter = Point(bounds.maxX - 30 + 8, bounds.minY + 25) val outputLevelLabel = Widgets.createLabel( outputLevelLabelCenter, - Text.literal("")).centered() + Component.literal("") + ).centered() val coinStack = SBItemStack(SkyblockId.COINS, recipe.coins.toInt()) levelValue.whenChanged { oldValue, newValue -> if (oldValue.toInt() == newValue.toInt()) return@whenChanged @@ -69,43 +72,63 @@ class SBKatRecipe(override val neuRecipe: NEUKatUpgradeRecipe) : SBRecipe() { 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.message = Component.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()) + inputLevelLabelCenter.x - MC.font.width(inputLevelLabel.message) / 2, + inputLevelLabelCenter.y + ) + outputLevelLabel.message = Component.literal(newOutput.levelData.currentLevel.toString()) outputLevelLabel.bounds.location = Point( - outputLevelLabelCenter.x - MC.font.getWidth(outputLevelLabel.message) / 2, - outputLevelLabelCenter.y) + outputLevelLabelCenter.x - MC.font.width(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( + 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)), + Component.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))) + 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)) + add( + Widgets.createSlot( + Point( + bounds.centerX + index * 20 - allInputs.size * 18 / 2 - (allInputs.size - 1) * 2 / 2, + bounds.minY + 20 + ) + ) + .markInput() + .entry(item) + ) } } } @@ -118,85 +141,88 @@ fun wrapWidget(bounds: Rectangle, component: GuiComponent): Widget { return bounds } - override fun children(): List<Element> { + override fun children(): List<GuiEventListener> { 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) + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + context.pose().pushMatrix() + context.pose().translate(bounds.minX.toFloat(), bounds.minY.toFloat()) component.render( GuiImmediateContext( - ModernRenderContext(context), + MoulConfigRenderContext(context), bounds.minX, bounds.minY, bounds.width, bounds.height, mouseX - bounds.minX, mouseY - bounds.minY, mouseX, mouseY, mouseX.toFloat(), mouseY.toFloat() - )) - context.matrices.pop() + ) + ) + context.pose().popMatrix() } 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() - )) + 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 mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + val mouseXInt = event.x.toInt() + val mouseYInt = event.y.toInt() + return component.mouseEvent( + MouseEvent.Click(event.button(), true), + GuiImmediateContext( + IMinecraft.INSTANCE.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + event.x.toFloat(), event.y.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 mouseReleased(event: MouseButtonEvent): Boolean { + val mouseXInt = event.x.toInt() + val mouseYInt = event.y.toInt() + return component.mouseEvent( + MouseEvent.Click(event.button(), false), + GuiImmediateContext( + IMinecraft.INSTANCE.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + event.x.toFloat(), event.y.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 mouseDragged(event: MouseButtonEvent, mouseX: Double, mouseY: Double): Boolean { + val mouseXInt = event.x.toInt() + val mouseYInt = event.y.toInt() + return 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, + event.x.toFloat(), event.y.toFloat() + ) + ) } @@ -208,15 +234,17 @@ fun wrapWidget(bounds: Rectangle, component: GuiComponent): Widget { ): 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() - )) + 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 index b595c23..927c199 100644 --- 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 @@ -9,9 +9,9 @@ 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 net.minecraft.world.item.Items +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.compat.rei.EntityWidget import moe.nea.firmament.compat.rei.SBItemEntryDefinition @@ -24,7 +24,7 @@ class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { override fun getCategoryIdentifier(): CategoryIdentifier<SBMobDropRecipe> = CategoryIdentifier.of(Firmament.MOD_ID, "mob_drop_recipe") - override fun getTitle(): Text = Text.literal("Mob Drops") + override fun getTitle(): Component = Component.literal("Mob Drops") override fun getDisplayHeight(): Int { return 100 } @@ -35,23 +35,23 @@ class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { add(Widgets.createRecipeBase(bounds)) val source = display.neuRecipe.render val entity = if (source.startsWith("@")) { - EntityRenderer.constructEntity(Identifier.of(source.substring(1))) + EntityRenderer.constructEntity(ResourceLocation.parse(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<Text>() + if (level > 0) Component.translatable("firmament.recipe.mobs.name", level, display.neuRecipe.name) + else Component.translatable("firmament.recipe.mobs.name.nolevel", display.neuRecipe.name) + val tt = mutableListOf<Component>() tt.add((fullMobName)) - tt.add(Text.literal("")) + tt.add(Component.literal("")) if (display.neuRecipe.coins > 0) { - tt.add(Text.stringifiedTranslatable("firmament.recipe.mobs.coins", display.neuRecipe.coins)) + tt.add(Component.translatable("firmament.recipe.mobs.coins", display.neuRecipe.coins)) } if (display.neuRecipe.combatExperience > 0) { tt.add( - Text.stringifiedTranslatable( + Component.translatable( "firmament.recipe.mobs.combat", display.neuRecipe.combatExperience ) @@ -59,14 +59,14 @@ class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { } if (display.neuRecipe.enchantingExperience > 0) { tt.add( - Text.stringifiedTranslatable( + Component.translatable( "firmament.recipe.mobs.exp", display.neuRecipe.enchantingExperience ) ) } if (display.neuRecipe.extra != null) - display.neuRecipe.extra.mapTo(tt) { Text.literal(it) } + display.neuRecipe.extra.mapTo(tt) { Component.literal(it) } if (tt.size == 2) tt.removeAt(1) add( @@ -76,15 +76,15 @@ class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { ) ) add( - Widgets.createLabel(Point(bounds.minX + 15, bounds.minY + 5), Text.literal(display.neuRecipe.name)) + Widgets.createLabel(Point(bounds.minX + 15, bounds.minY + 5), Component.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) } + val lore = drop.extra.mapTo(mutableListOf()) { Component.literal(it) } if (drop.chance != null) { - lore += listOf(Text.translatable("firmament.recipe.mobs.drops", drop.chance)) + lore += listOf(Component.translatable("firmament.recipe.mobs.drops", drop.chance)) } val item = SBItemEntryDefinition.getEntry(drop.dropItem) .value.copy(extraLore = lore) 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 index de7779f..d6ca3f6 100644 --- 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 @@ -6,12 +6,12 @@ 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 net.minecraft.resources.ResourceLocation import moe.nea.firmament.compat.rei.SBItemEntryDefinition import moe.nea.firmament.util.SkyblockId abstract class SBRecipe : Display { - override fun getDisplayLocation(): Optional<Identifier> { + override fun getDisplayLocation(): Optional<ResourceLocation> { // 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() } 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 index 4d00a4f..ca69782 100644 --- 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 @@ -1,3 +1,5 @@ +@file:OptIn(ExpensiveItemCacheApi::class) + package moe.nea.firmament.compat.rei.recipes import java.util.Optional @@ -17,15 +19,17 @@ 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.text.Text -import net.minecraft.util.Identifier -import net.minecraft.village.VillagerProfession +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntitySpawnReason +import net.minecraft.core.Holder +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.npc.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 @@ -33,6 +37,7 @@ 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 @@ -44,7 +49,7 @@ import moe.nea.firmament.util.tr class SBReforgeRecipe( val reforge: Reforge, - val limitToItem: SkyblockId?, + val limitToItem: SBItemStack?, ) : Display { companion object { val catIdentifier = CategoryIdentifier.of<SBReforgeRecipe>(Firmament.MOD_ID, "reforge_recipe") @@ -55,7 +60,7 @@ class SBReforgeRecipe( return catIdentifier } - override fun getTitle(): Text { + override fun getTitle(): Component { return tr("firmament.recipecategory.reforge", "Reforge") } @@ -69,37 +74,6 @@ class SBReforgeRecipe( 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(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<Pair<String, Label>>() @@ -124,6 +98,37 @@ class SBReforgeRecipe( } updateStatLines() inputSlot.withEntriesListener { updateStatLines() } + 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.wrapRenderer( + Rectangle(Point(bounds.minX + 10 + 24, bounds.centerY - 9 + 10), Dimension(16, 16)), + SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)), + Rarity.entries.mapNotNull { rarity -> + display.reforge.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Component.literal("") + .append(rarity.text) + .append(": ") + .append(Component.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, EntitySpawnReason.COMMAND) + ?.also { it.villagerData = it.villagerData.withProfession(MC.currentOrDefaultRegistries, 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() + )) + } return list } } @@ -132,10 +137,10 @@ class SBReforgeRecipe( fun getRecipesForSBItemStack(item: SBItemStack): Optional<List<SBReforgeRecipe>> { val reforgeRecipes = mutableListOf<SBReforgeRecipe>() for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) { - reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId)) + reforgeRecipes.add(SBReforgeRecipe(reforge, item)) } for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) { - reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId)) + reforgeRecipes.add(SBReforgeRecipe(reforge, item)) } if (reforgeRecipes.isEmpty()) return Optional.empty() return Optional.of(reforgeRecipes) @@ -162,26 +167,27 @@ class SBReforgeRecipe( } } - private val eligibleItems = - if (limitToItem != null) listOfNotNull(RepoManager.getNEUItem(limitToItem)) - else reforge.eligibleItems.flatMap { + 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 + 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 + } } - } } - private val inputItems = eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) } + 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) @@ -201,8 +207,8 @@ class SBReforgeRecipe( return catIdentifier } - override fun getDisplayLocation(): Optional<Identifier> { - return Optional.empty() + override fun getDisplayLocation(): Optional<ResourceLocation> { + return Optional.empty<ResourceLocation>() } override fun getSerializer(): DisplaySerializer<out Display>? { 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..87d8bdf --- /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.world.item.Items +import net.minecraft.network.chat.Component +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<EntryIngredient> { + return listOf(EntryIngredient.of(merchant)) + super.getInputEntries() + } + + object Category : DisplayCategory<SBShopRecipe> { + val catIdentifier = CategoryIdentifier.of<SBShopRecipe>(Firmament.MOD_ID, "npc_shopping") + override fun getCategoryIdentifier(): CategoryIdentifier<SBShopRecipe> = catIdentifier + + override fun getTitle(): Component = Component.literal("SkyBlock NPC Shopping") + + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.EMERALD) + override fun setupDisplay(display: SBShopRecipe, bounds: Rectangle): List<Widget> { + 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/sodium/java/moe/nea/firmament/compat/sodium/Compat.kt b/src/compat/sodium/java/moe/nea/firmament/compat/sodium/Compat.kt new file mode 100644 index 0000000..97ae1fc --- /dev/null +++ b/src/compat/sodium/java/moe/nea/firmament/compat/sodium/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.sodium + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta +import net.fabricmc.loader.api.FabricLoader + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("sodium") + } +} diff --git a/src/compat/sodium/java/SodiumChunkReloader.kt b/src/compat/sodium/java/moe/nea/firmament/compat/sodium/SodiumChunkReloader.kt index 0256b88..e5d0fd9 100644 --- a/src/compat/sodium/java/SodiumChunkReloader.kt +++ b/src/compat/sodium/java/moe/nea/firmament/compat/sodium/SodiumChunkReloader.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.compat.sodium +import moe.nea.firmament.mixins.sodium.accessor.AccessorSodiumWorldRenderer import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer -import moe.nea.firmament.mixins.accessor.sodium.AccessorSodiumWorldRenderer class SodiumChunkReloader : Runnable { override fun run() { 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 deleted file mode 100644 index fe87310..0000000 --- a/src/compat/sodium/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java +++ /dev/null @@ -1,29 +0,0 @@ -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<BakedModel> 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/sodium/java/moe/nea/firmament/mixins/accessor/sodium/AccessorSodiumWorldRenderer.java b/src/compat/sodium/java/moe/nea/firmament/mixins/sodium/accessor/AccessorSodiumWorldRenderer.java index f75874d..bc2210a 100644 --- a/src/compat/sodium/java/moe/nea/firmament/mixins/accessor/sodium/AccessorSodiumWorldRenderer.java +++ b/src/compat/sodium/java/moe/nea/firmament/mixins/sodium/accessor/AccessorSodiumWorldRenderer.java @@ -1,4 +1,4 @@ -package moe.nea.firmament.mixins.accessor.sodium; +package moe.nea.firmament.mixins.sodium.accessor; import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionManager; diff --git a/src/compat/sodium/java/moe/nea/firmament/mixins/sodium/custommodels/PatchBlockModelInSodiumChunkGenerator.java b/src/compat/sodium/java/moe/nea/firmament/mixins/sodium/custommodels/PatchBlockModelInSodiumChunkGenerator.java new file mode 100644 index 0000000..50dba85 --- /dev/null +++ b/src/compat/sodium/java/moe/nea/firmament/mixins/sodium/custommodels/PatchBlockModelInSodiumChunkGenerator.java @@ -0,0 +1,29 @@ +package moe.nea.firmament.mixins.sodium.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.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.renderer.block.model.BlockStateModel; +import net.minecraft.core.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/renderer/block/BlockModelShaper;getBlockModel(Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/client/renderer/block/model/BlockStateModel;")) + private BlockStateModel replaceBlockModel(BlockModelShaper instance, BlockState state, Operation<BlockStateModel> original, + @Local(name = "blockPos") BlockPos.MutableBlockPos 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 index c3e8950..f8999da 100644 --- 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 @@ -4,9 +4,9 @@ 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 net.minecraft.world.item.equipment.Equippable; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.injection.At; @@ -14,10 +14,10 @@ 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;")) + @ModifyExpressionValue(method = "render(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/client/renderer/entity/state/HumanoidRenderState;FF)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;get(Lnet/minecraft/core/component/DataComponentType;)Ljava/lang/Object;")) private Object replaceArmorMaterial(Object original, @Local ItemStack chestplate) { var overrides = CustomGlobalArmorOverrides.overrideArmor(chestplate, EquipmentSlot.CHEST); - return overrides.orElse((EquippableComponent) original); + return overrides.orElse((Equippable) original); } } diff --git a/src/compat/yacl/java/KeybindingController.kt b/src/compat/yacl/java/KeybindingController.kt index 204d521..3714455 100644 --- a/src/compat/yacl/java/KeybindingController.kt +++ b/src/compat/yacl/java/KeybindingController.kt @@ -6,10 +6,13 @@ 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 net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.network.chat.Component 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.GenericInputButton import moe.nea.firmament.keybindings.SavedKeyBinding class KeybindingController( @@ -21,7 +24,7 @@ class KeybindingController( return option } - override fun formatValue(): Text { + override fun formatValue(): Component { return option.pendingValue().format() } @@ -52,16 +55,16 @@ class KeybindingWidget( return 130 } - override fun getValueText(): Text { + override fun getValueText(): Component { return sm.label } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return sm.keyboardEvent(keyCode, true) + override fun keyPressed(keyEvent: KeyEvent): Boolean { + return sm.keyboardEvent(GenericInputButton.of(keyEvent), true) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return sm.keyboardEvent(keyCode, false) + override fun keyReleased(keyEvent: KeyEvent): Boolean { + return sm.keyboardEvent(GenericInputButton.of(keyEvent), false) } override fun unfocus() { @@ -73,11 +76,11 @@ class KeybindingWidget( if (!focused) sm.onLostFocus() } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - if (button == 0 && isHovered) { - sm.onClick() + override fun mouseClicked(mouseButtonEvent: MouseButtonEvent, doubleClick: Boolean): Boolean { + if (isHovered) { + sm.onClick(mouseButtonEvent.button()) return true } - return super.mouseClicked(mouseX, mouseY, button) + return super.mouseClicked(mouseButtonEvent, doubleClick) } } diff --git a/src/compat/yacl/java/YaclIntegration.kt b/src/compat/yacl/java/YaclIntegration.kt index 45a0d02..e10b8c4 100644 --- a/src/compat/yacl/java/YaclIntegration.kt +++ b/src/compat/yacl/java/YaclIntegration.kt @@ -9,6 +9,7 @@ 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 @@ -17,16 +18,19 @@ 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 net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.components.AbstractContainerWidget +import net.minecraft.network.chat.Component 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 @@ -34,11 +38,13 @@ 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.util.data.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) @@ -56,20 +62,22 @@ class YaclIntegration : FirmamentConfigScreenProvider { OptionGroup.createBuilder() .name(it.labelText) .options(buildOptions(it.sortedOptions)) - .build()) + .build() + ) } } .build() } fun buildOptions(options: List<ManagedOption<*>>): Collection<Option<*>> = - options.map { buildOption(it) } + options.flatMap { buildOption(it) } - private fun <T : Any> buildOption(managedOption: ManagedOption<T>): Option<*> { + private fun <T : Any> buildOption(managedOption: ManagedOption<T>): Collection<Option<*>> { val handler = managedOption.handler - val binding = Binding.generic(managedOption.default(), - managedOption::value, - { managedOption.value = it; managedOption.element.save() }) + val binding = Binding.generic( + managedOption.default(), + managedOption::value, + { managedOption.value = it; managedOption.element.markDirty() }) fun <T> createDefaultBinding(function: (Option<T>) -> ControllerBuilder<T>): Option.Builder<T> { return Option.createBuilder<T>() @@ -78,47 +86,89 @@ class YaclIntegration : FirmamentConfigScreenProvider { .binding(binding as Binding<T>) .controller { function(it) } } + + fun Option<out Any>.single() = listOf(this) + fun ButtonOption.Builder.single() = build().single() + fun Option.Builder<out Any>.single() = build().single() when (handler) { is ClickHandler -> return ButtonOption.createBuilder() .name(managedOption.labelText) .action { t, u -> handler.runnable() } - .build() + .single() is HudMetaHandler -> return ButtonOption.createBuilder() .name(managedOption.labelText) .action { t, u -> handler.openEditor(managedOption as ManagedOption<HudMeta>, t) } - .build() + .single() is ChoiceHandler<*> -> return createDefaultBinding { createChoiceBinding(handler as ChoiceHandler<*>, managedOption as ManagedOption<*>, it as Option<*>) - }.build() + }.single() + + is ColourHandler -> { + managedOption as ManagedOption<ChromaColour> + val colorBinding = + Binding.generic( + managedOption.default().getRGBAWithoutAnimation(), + { managedOption.value.getRGBAWithoutAnimation() }, + { + managedOption.value = + it.toChromaWithoutAnimation(managedOption.value.timeForFullRotationInMillis) + managedOption.element.markDirty() + }) + val speedBinding = + Binding.generic( + managedOption.default().timeForFullRotationInMillis, + { managedOption.value.timeForFullRotationInMillis }, + { + managedOption.value = managedOption.value.copy(timeForFullRotationInMillis = it) + managedOption.element.markDirty() + } + ) + + return listOf( + Option.createBuilder<Color>() + .name(managedOption.labelText) + .binding(colorBinding) + .controller { + ColorControllerBuilder.create(it) + .allowAlpha(true) + } + .build(), + Option.createBuilder<Int>() + .name(managedOption.labelText) + .binding(speedBinding) + .controller { IntegerSliderControllerBuilder.create(it).range(0, 60_000).step(10) } + .build(), + ) + } - is BooleanHandler -> return createDefaultBinding(TickBoxControllerBuilder::create).build() - is StringHandler -> return createDefaultBinding(StringControllerBuilder::create).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) - }.build() + }.single() is DurationHandler -> return Option.createBuilder<Double>() .name(managedOption.labelText) .binding((binding as Binding<Duration>).xmap({ it.toDouble(DurationUnit.SECONDS) }, { it.seconds })) .controller { DoubleSliderControllerBuilder.create(it) - .formatValue { Text.literal(FirmFormatters.formatTimespan(it.seconds)) } + .formatValue { Component.literal(FirmFormatters.formatTimespan(it.seconds)) } .step(0.1) .range(handler.min.toDouble(DurationUnit.SECONDS), handler.max.toDouble(DurationUnit.SECONDS)) } - .build() + .single() is KeyBindingHandler -> return createDefaultBinding { KeybindingBuilder(it, managedOption as ManagedOption<SavedKeyBinding>) - }.build() + }.single() - else -> return LabelOption.create(Text.literal("This option is currently unhandled for this config menu. Please report this as a bug.")) + else -> return listOf(LabelOption.create(Component.literal("This option is currently unhandled for this config menu. Please report this as a bug."))) } } @@ -146,7 +196,7 @@ class YaclIntegration : FirmamentConfigScreenProvider { fun buildConfig(): YetAnotherConfigLib { return YetAnotherConfigLib.createBuilder() - .title(Text.literal("Firmament")) + .title(Component.literal("Firmament")) .categories(buildCategories()) .build() } @@ -154,11 +204,11 @@ class YaclIntegration : FirmamentConfigScreenProvider { override val key: String get() = "yacl" - override fun open(parent: Screen?): Screen { + override fun open(search: String?, parent: Screen?): Screen { return object : YACLScreen(buildConfig(), parent) { - override fun setFocused(focused: Element?) { + override fun setFocused(focused: GuiEventListener?) { if (this.focused is KeybindingWidget && - focused is ListHolderWidget<*> + focused is AbstractContainerWidget ) { return } diff --git a/src/gametest/kotlin/moe/nea/firmament/gametest/GameTest.kt b/src/gametest/kotlin/moe/nea/firmament/gametest/GameTest.kt new file mode 100644 index 0000000..0ef0340 --- /dev/null +++ b/src/gametest/kotlin/moe/nea/firmament/gametest/GameTest.kt @@ -0,0 +1,24 @@ +package moe.nea.firmament.gametest + +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext +import org.junit.jupiter.api.Assertions +import org.spongepowered.asm.mixin.MixinEnvironment +import moe.nea.firmament.init.MixinPlugin +import moe.nea.firmament.test.FirmTestBootstrap + +class GameTest : FabricClientGameTest { + override fun runTest(ctx: ClientGameTestContext) { + FirmTestBootstrap.bootstrapMinecraft() + MixinEnvironment.getCurrentEnvironment().audit() + val mp = MixinPlugin.instances.single() + Assertions.assertEquals( + mp.expectedFullPathMixins, + mp.appliedFullPathMixins, + ) + Assertions.assertNotEquals( + 0, + mp.mixins.size + ) + } +} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json new file mode 100644 index 0000000..fb1174d --- /dev/null +++ b/src/gametest/resources/fabric.mod.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "id": "firmament-gametest", + "version": "1.0.0", + "name": "Firmament Game Tests", + "environment": "*", + "entrypoints": { + "fabric-gametest": [ + ], + "fabric-client-gametest": [ + "moe.nea.firmament.gametest.GameTest" + ] + } +} diff --git a/src/main/java/moe/nea/firmament/gui/config/storage/ArrayIndexedJsonPointer.kt b/src/main/java/moe/nea/firmament/gui/config/storage/ArrayIndexedJsonPointer.kt new file mode 100644 index 0000000..1e204d6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/gui/config/storage/ArrayIndexedJsonPointer.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.gui.config.storage + +import com.google.gson.JsonArray +import com.google.gson.JsonElement + +data class ArrayIndexedJsonPointer( + val owner: JsonArray, + val index: Int +) : JsonPointer { + override fun get(): JsonElement { + return owner.get(index) + } + + override fun set(value: JsonElement) { + owner.set(index, value) + } +} diff --git a/src/main/java/moe/nea/firmament/gui/config/storage/ConfigEditor.kt b/src/main/java/moe/nea/firmament/gui/config/storage/ConfigEditor.kt new file mode 100644 index 0000000..df1ed33 --- /dev/null +++ b/src/main/java/moe/nea/firmament/gui/config/storage/ConfigEditor.kt @@ -0,0 +1,104 @@ +package moe.nea.firmament.gui.config.storage + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import kotlinx.serialization.json.JsonElement +import moe.nea.firmament.util.json.intoGson +import moe.nea.firmament.util.json.intoKotlinJson + +data class ConfigEditor( + val roots: List<JsonPointer>, +) { + fun transform(transform: (JsonElement) -> JsonElement) { + roots.forEach { root -> + root.set(transform(root.get().intoKotlinJson()).intoGson()) + } + } + + fun move(fromPath: String, toPath: String) { + if (fromPath == toPath) return + val fromSegments = fromPath.split(".").filter { it.isNotEmpty() } + val toSegments = toPath.split(".").filter { it.isNotEmpty() } + roots.forEach { root -> + var fp = root.get() + if (fromSegments.isEmpty()) { + root.set(JsonObject()) + } else { + fromSegments.dropLast(1).forEach { + fp = (fp as JsonObject)[it] ?: return@forEach // todo warn if we dont find the object maybe + } + fp as JsonObject + fp = fp.remove(fromSegments.last())?.deepCopy() ?: return@forEach // in theory i don't need to deepcopy but fuck theory + } + if (toSegments.isEmpty()) { + root.set(fp) + } else { + var lp = root.get() + toSegments.dropLast(1).forEach { name -> + val parent = lp as JsonObject + var child = parent[name] + if (child == null) { + child = JsonObject() + parent.add(name, child) + } + lp = child + } + lp as JsonObject + if (lp.has(toSegments.last())) { + error("Cannot overwrite $lp.${toSegments.last()} with $fp") + } + lp.add(toSegments.last(), fp) + } + } + } + + fun at(path: String, block: ConfigEditor.() -> Unit) { + block(at(path)) + } + + fun at(path: String): ConfigEditor { + var lastRoots = roots + for (segment in path.split(".")) { + if (segment.isEmpty()) { + continue + } else if (segment == "*") { + lastRoots = lastRoots.flatMap { root -> + when (val ele = root.get()) { + is JsonObject -> { + ele.entrySet().map { + (ObjectIndexedJsonPointer(ele, it.key)) + } + } + + is JsonArray -> { + (0..<ele.size()).map { + (ArrayIndexedJsonPointer(ele, it)) + } + } + + else -> { + error("Cannot expand a json primitive $ele at $path") + } + } + } + } else { + lastRoots = lastRoots.map { root -> + when (val ele = root.get()) { + is JsonObject -> { + ObjectIndexedJsonPointer(ele, segment) + } + + is JsonArray -> { + ArrayIndexedJsonPointer(ele, segment.toInt()) + } + + else -> { + error("Cannot expand a json primitive $ele at $path") + } + } + } + } + } + return ConfigEditor(lastRoots) + } +} diff --git a/src/main/java/moe/nea/firmament/gui/config/storage/ConfigFixEvent.kt b/src/main/java/moe/nea/firmament/gui/config/storage/ConfigFixEvent.kt new file mode 100644 index 0000000..07148d5 --- /dev/null +++ b/src/main/java/moe/nea/firmament/gui/config/storage/ConfigFixEvent.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.gui.config.storage + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.firmament.events.FirmamentEvent +import moe.nea.firmament.events.FirmamentEventBus + +data class ConfigFixEvent( + val storageClass: ConfigStorageClass, + val toVersion: Int, + var data: JsonObject, +) : FirmamentEvent() { + companion object : FirmamentEventBus<ConfigFixEvent>() { + + } + fun on( + toVersion: Int, + storageClass: ConfigStorageClass, + block: ConfigEditor.() -> Unit + ) { + require(toVersion <= FirmamentConfigLoader.currentConfigVersion) + if (this.toVersion == toVersion && this.storageClass == storageClass) { + block(ConfigEditor(listOf(object : JsonPointer { + override fun get(): JsonObject { + return data + } + + override fun set(value: JsonElement) { + data = value as JsonObject + } + + override fun toString(): String { + return "ConfigRoot($storageClass)" + } + }))) + } + } +} diff --git a/src/main/java/moe/nea/firmament/gui/config/storage/JsonPointer.kt b/src/main/java/moe/nea/firmament/gui/config/storage/JsonPointer.kt new file mode 100644 index 0000000..e34c312 --- /dev/null +++ b/src/main/java/moe/nea/firmament/gui/config/storage/JsonPointer.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.gui.config.storage + +import com.google.gson.JsonElement + +interface JsonPointer { + fun get(): JsonElement + fun set(value: JsonElement) +} diff --git a/src/main/java/moe/nea/firmament/gui/config/storage/ObjectIndexedJsonPointer.kt b/src/main/java/moe/nea/firmament/gui/config/storage/ObjectIndexedJsonPointer.kt new file mode 100644 index 0000000..091275d --- /dev/null +++ b/src/main/java/moe/nea/firmament/gui/config/storage/ObjectIndexedJsonPointer.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.gui.config.storage + +import com.google.gson.JsonElement +import com.google.gson.JsonObject + +data class ObjectIndexedJsonPointer( + val owner: JsonObject, + val name: String +) : JsonPointer { + override fun get(): JsonElement { + return owner.get(name) + } + + override fun set(value: JsonElement) { + owner.add(name, value) + } +} diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java index 0713068..07e4549 100644 --- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java +++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java @@ -1,6 +1,9 @@ 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; @@ -24,6 +27,8 @@ public class AutoDiscoveryPlugin { return mixins.stream().map(it -> defaultName + "." + it).toList(); } + // TODO: remove println + private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>(); public static List<AutoDiscoveryPlugin> getMixinPlugins() { @@ -94,7 +99,7 @@ public class AutoDiscoveryPlugin { String norm = (className.substring(0, className.length() - ".class".length())) .replace("\\", "/") .replace("/", "."); - if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) { mixins.add(norm.substring(getMixinPackage().length() + 1)); } } @@ -125,24 +130,25 @@ public class AutoDiscoveryPlugin { */ public List<String> getMixins() { if (mixins != null) return mixins; - 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; - } - try { + 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 (MalformedURLException e) { - throw new RuntimeException(e); } } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); } return mixins; } diff --git a/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java b/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java deleted file mode 100644 index d60e3e7..0000000 --- a/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java +++ /dev/null @@ -1,75 +0,0 @@ -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, "<init>") && Type.getMethodType(method.desc).equals(constructorDescriptor)) { - modifyConstructor(method, superClass); - return; - } - } - var node = new MethodNode(Opcodes.ASM9, "<init>", 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(), "<init>", 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 index 5441255..ae26bd7 100644 --- a/src/main/java/moe/nea/firmament/init/EarlyRiser.java +++ b/src/main/java/moe/nea/firmament/init/EarlyRiser.java @@ -4,7 +4,6 @@ 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 index f7db18c..98be517 100644 --- a/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java +++ b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java @@ -2,7 +2,13 @@ package moe.nea.firmament.init; import me.shedaniel.mm.api.ClassTinkerers; -import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; @@ -19,30 +25,46 @@ 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.<Element>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.<Element>className(), - Intermediary.methodName(Element::charTyped), - charTypedDesc.getDescriptor()); + Intermediary.InterClass Screen = Intermediary.<Screen>intermediaryClass(); + Intermediary.InterClass KeyInput = Intermediary.<KeyEvent>intermediaryClass(); + Intermediary.InterClass CharInput = Intermediary.<CharacterEvent>intermediaryClass(); + Intermediary.InterClass HandledScreen = Intermediary.<AbstractContainerScreen>intermediaryClass(); + Intermediary.InterClass AbstractContainerEventHandler = Intermediary.<AbstractContainerEventHandler>intermediaryClass(); + Intermediary.InterClass MouseButtonEvent = Intermediary.<MouseButtonEvent>intermediaryClass(); + Intermediary.InterMethod mouseScrolled = Intermediary.intermediaryMethod( + GuiEventListener::mouseScrolled, + Intermediary.ofClass(boolean.class), + Intermediary.ofClass(double.class), + Intermediary.ofClass(double.class), + Intermediary.ofClass(double.class), + Intermediary.ofClass(double.class) + ); + Intermediary.InterMethod mouseClickedScreen = Intermediary.intermediaryMethod( + //onMouseClicked$firmament + GuiEventListener::mouseClicked, + Intermediary.ofClass(boolean.class), + MouseButtonEvent, + Intermediary.ofClass(boolean.class) + ); + ; + Intermediary.InterMethod keyReleased = Intermediary.intermediaryMethod( + GuiEventListener::keyReleased, + Intermediary.ofClass(boolean.class), + KeyInput + ); + Intermediary.InterMethod charTyped = Intermediary.intermediaryMethod( + GuiEventListener::charTyped, + Intermediary.ofClass(boolean.class), + CharInput + ); @Override public void addTinkerers() { - ClassTinkerers.addTransformation(HandledScreen, this::addMouseScroll, true); - ClassTinkerers.addTransformation(HandledScreen, this::addKeyReleased, true); - ClassTinkerers.addTransformation(HandledScreen, this::addCharTyped, true); + addTransformation(HandledScreen, this::addMouseScroll, true); + addTransformation(HandledScreen, this::addKeyReleased, true); + addTransformation(HandledScreen, this::addCharTyped, true); + addTransformation(Screen, this::addMouseClicked, true); } /** @@ -56,8 +78,8 @@ public class HandledScreenRiser extends RiserUtils { * @param insertInvoke insert the invokevirtual/invokestatic call */ void insertTrueHandler(MethodNode node, - Consumer<InsnList> insertLoads, - Consumer<InsnList> insertInvoke) { + Consumer<InsnList> insertLoads, + Consumer<InsnList> insertInvoke) { var insns = new InsnList(); insertLoads.accept(insns); @@ -76,26 +98,39 @@ public class HandledScreenRiser extends RiserUtils { void addKeyReleased(ClassNode classNode) { addSuperInjector( - classNode, keyReleased, keyReleasedDesc, "keyReleased_firmament", + classNode, keyReleased.mapped(), keyReleased.mappedDesc(), HandledScreen, Screen, "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)); + // ALOAD 1, load args + insns.add(new VarInsnNode(Opcodes.ALOAD, 1)); }); } + void addMouseClicked(ClassNode classNode) { + addSuperInjector( + classNode, mouseClickedScreen.mapped(), mouseClickedScreen.mappedDesc(), + Screen, AbstractContainerEventHandler, "onMouseClicked$firmament", + insns -> { + // load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // load mouse event + insns.add(new VarInsnNode(Opcodes.ALOAD, 1)); + // load doubled + insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); + } + ); + } + void addCharTyped(ClassNode classNode) { addSuperInjector( - classNode, charTyped, charTypedDesc, "charTyped_firmament", + classNode, charTyped.mapped(), charTyped.mappedDesc(), + HandledScreen, Screen, "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)); + // ALOAD 1, load args + insns.add(new VarInsnNode(Opcodes.ALOAD, 1)); }); } @@ -103,6 +138,8 @@ public class HandledScreenRiser extends RiserUtils { ClassNode classNode, String name, Type desc, + Intermediary.InterClass currentClass, + Intermediary.InterClass parentClass, String firmamentName, Consumer<InsnList> loadArgs ) { @@ -118,8 +155,8 @@ public class HandledScreenRiser extends RiserUtils { var insns = keyReleasedNode.instructions; loadArgs.accept(insns); // INVOKESPECIAL call super method - insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(), - name, desc.getDescriptor())); + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, parentClass.mapped().getInternalName(), + name, desc.getDescriptor())); // IRETURN return int on stack (booleans are int at runtime) insns.add(new InsnNode(Opcodes.IRETURN)); classNode.methods.add(keyReleasedNode); @@ -127,16 +164,16 @@ public class HandledScreenRiser extends RiserUtils { insertTrueHandler(keyReleasedNode, loadArgs, insns -> { // INVOKEVIRTUAL call custom handler insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, - getTypeForClassName(HandledScreen).getInternalName(), - firmamentName, - desc.getDescriptor())); + currentClass.mapped().getInternalName(), + firmamentName, + desc.getDescriptor())); }); } void addMouseScroll(ClassNode classNode) { addSuperInjector( - classNode, mouseScrolled, mouseScrolledDesc, "mouseScrolled_firmament", + classNode, mouseScrolled.mapped(), mouseScrolled.mappedDesc(), HandledScreen, Screen, "mouseScrolled_firmament", insns -> { // ALOAD 0, load this insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); diff --git a/src/main/java/moe/nea/firmament/init/Intermediary.java b/src/main/java/moe/nea/firmament/init/Intermediary.java index 61494d7..3513a6b 100644 --- a/src/main/java/moe/nea/firmament/init/Intermediary.java +++ b/src/main/java/moe/nea/firmament/init/Intermediary.java @@ -7,57 +7,66 @@ 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 <T> 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<Type> 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)); -// } -// -// -// } + private static final MappingResolver RESOLVER = FabricLoader.getInstance().getMappingResolver(); + + static InterMethod intermediaryMethod(Object object, InterClass returnType, InterClass... args) { + throw new AssertionError("Cannot be called at runtime"); + } + + static <T> InterClass intermediaryClass() { + throw new AssertionError("Cannot be called at runtime"); + } + + public static InterClass ofIntermediaryClass(String interClass) { + return new InterClass(Type.getObjectType(interClass.replace('.', '/'))); + } + + public static InterClass ofClass(Class<?> unmappedClass) { + return new InterClass(Type.getType(unmappedClass)); + } + + public static InterMethod ofMethod(String intermediary, String ownerType, InterClass returnType, InterClass... argTypes) { + return new InterMethod(intermediary, ofIntermediaryClass(ownerType), returnType, List.of(argTypes)); + } + + public record InterClass( + Type intermediary + ) { + public Type mapped() { + if (intermediary().getSort() != Type.OBJECT) + return intermediary(); + return Type.getObjectType(RESOLVER.mapClassName("intermediary", intermediary().getClassName()) + .replace('.', '/')); + } + } + + public record InterMethod( + String intermediary, + InterClass ownerType, + InterClass returnType, + List<InterClass> argumentTypes + ) { + public Type intermediaryDesc() { + return Type.getMethodType( + returnType.intermediary(), + argumentTypes().stream().map(InterClass::intermediary).toArray(Type[]::new) + ); + } + + public Type mappedDesc() { + return Type.getMethodType( + returnType.mapped(), + argumentTypes().stream().map(InterClass::mapped).toArray(Type[]::new) + ); + } + + public String mapped() { + return RESOLVER.mapMethodName( + "intermediary", + ownerType.intermediary().getClassName(), + intermediary(), + intermediaryDesc().getDescriptor() + ); + } + } } diff --git a/src/main/java/moe/nea/firmament/init/MixinPlugin.java b/src/main/java/moe/nea/firmament/init/MixinPlugin.java index 61e8f14..d48139b 100644 --- a/src/main/java/moe/nea/firmament/init/MixinPlugin.java +++ b/src/main/java/moe/nea/firmament/init/MixinPlugin.java @@ -8,54 +8,69 @@ 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 String mixinPackage; - @Override - public void onLoad(String mixinPackage) { - MixinExtrasBootstrap.init(); - MixinPlugin.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<String> myTargets, Set<String> otherTargets) { - - } - - @Override - public List<String> 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 static List<String> appliedMixins = new ArrayList<>(); - - @Override - public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - appliedMixins.add(mixinClassName); - } + AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin(); + public static List<MixinPlugin> 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<String> myTargets, Set<String> otherTargets) { + + } + + @Override + public List<String> 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<String> getAppliedFullPathMixins() { + return new HashSet<>(appliedMixins); + } + + public Set<String> getExpectedFullPathMixins() { + return getMixins() + .stream() + .map(it -> mixinPackage + "." + it) + .collect(Collectors.toSet()); + } + + public List<String> 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 index c1c8fd1..ad4ac8f 100644 --- a/src/main/java/moe/nea/firmament/init/RiserUtils.java +++ b/src/main/java/moe/nea/firmament/init/RiserUtils.java @@ -1,12 +1,15 @@ package moe.nea.firmament.init; +import me.shedaniel.mm.api.ClassTinkerers; 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; +import java.util.function.Consumer; + public abstract class RiserUtils { protected Type getTypeForClassName(String className) { return Type.getObjectType(className.replace('.', '/')); @@ -24,4 +27,8 @@ public abstract class RiserUtils { return null; } + public void addTransformation(Intermediary.InterClass interClass, Consumer<ClassNode> transformer, boolean post) { + ClassTinkerers.addTransformation(interClass.mapped().getClassName(), transformer, post); + } + } diff --git a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java index f2c6c53..a5d5c1d 100644 --- a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java +++ b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java @@ -1,13 +1,11 @@ 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.BlockModels; -import net.minecraft.client.render.block.BlockRenderManager; -import net.minecraft.client.render.chunk.SectionBuilder; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.util.math.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.chunk.SectionCompiler; +import net.minecraft.client.renderer.block.model.BlockStateModel; +import net.minecraft.core.BlockPos; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; @@ -20,98 +18,89 @@ 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(BakedModel.class) - String BakedModel; - String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures"; + Intermediary.InterClass SectionBuilder = Intermediary.<SectionCompiler>intermediaryClass(); + Intermediary.InterClass BlockPos = Intermediary.<BlockPos>intermediaryClass(); + Intermediary.InterClass BlockRenderManager = Intermediary.<BlockRenderDispatcher>intermediaryClass(); + Intermediary.InterClass BlockState = Intermediary.<BlockState>intermediaryClass(); + Intermediary.InterClass BlockStateModel = Intermediary.<BlockStateModel>intermediaryClass(); + String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures"; - Type getModelDesc = Type.getMethodType( - getTypeForClassName(BlockRenderManager), - getTypeForClassName(BlockState) - ); - String getModel = remapper.mapMethodName( - "intermediary", - Intermediary.<BlockRenderManager>className(), - Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel), - Type.getMethodDescriptor( - getTypeForClassName(Intermediary.<BakedModel>className()), - getTypeForClassName(Intermediary.<BlockState>className()) - ) - ); + Intermediary.InterMethod getModel = + Intermediary.intermediaryMethod( + net.minecraft.client.renderer.block.BlockRenderDispatcher::getBlockModel, + BlockStateModel, + BlockState + ); - @Override - public void addTinkerers() { - if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo")) - ClassTinkerers.addTransformation(SectionBuilder, this::handle, true); - } + @Override + public void addTinkerers() { + if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo")) + addTransformation(SectionBuilder, this::handle, true); + } - private void handle(ClassNode classNode) { - 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 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) - )); + private void handleIndigo(MethodNode method) { + LocalVariableNode blockPosVar = null, blockStateVar = null; + for (LocalVariableNode localVariable : method.localVariables) { + if (Type.getType(localVariable.desc).equals(BlockPos.mapped())) { + blockPosVar = localVariable; + } + if (Type.getType(localVariable.desc).equals(BlockState.mapped())) { + 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.mapped()) && + Type.getObjectType(methodInsn.owner).equals(BlockRenderManager.mapped()))) + 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(BakedModel), - getTypeForClassName(BakedModel), - getTypeForClassName(BlockPos), - getTypeForClassName(BlockState)), - false - )); - method.instructions.insert(methodInsn, insnList); - } - } + 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( + (BlockStateModel).mapped(), + (BlockStateModel).mapped(), + (BlockPos).mapped(), + (BlockState).mapped()), + false + )); + method.instructions.insert(methodInsn, insnList); + } + } } diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java index 22ce991..30b5020 100644 --- a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java +++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java @@ -1,28 +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.minecraft.resource.ResourceType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.packs.PackType; 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 = "appendModResourcePacks", at = @At("TAIL")) - private static void onAppendModResourcePack( - List<ModResourcePack> packs, - ResourceType type, - @Nullable String subPath, - CallbackInfo ci - ) { - RepoModResourcePack.Companion.append(packs); - } + @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, PackType type, @Nullable String subPath, CallbackInfoReturnable<List<ModResourcePack>> 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 index d898c44..8e591bd 100644 --- a/src/main/java/moe/nea/firmament/mixins/BandAidResourcePackPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/BandAidResourcePackPatch.java @@ -4,22 +4,22 @@ 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 net.minecraft.server.packs.resources.ReloadableResourceManager; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import java.util.Optional; -@Mixin(ReloadableResourceManagerImpl.class) +@Mixin(ReloadableResourceManager.class) public class BandAidResourcePackPatch { @ModifyReturnValue( method = "getResource", at = @At("RETURN") ) - private Optional<Resource> injectOurCustomResourcesInCaseExistingMethodsFailed(Optional<Resource> original, @Local Identifier identifier) { + private Optional<Resource> injectOurCustomResourcesInCaseExistingMethodsFailed(Optional<Resource> original, @Local ResourceLocation identifier) { return original.or(() -> RepoModResourcePack.Companion.createResourceDirectly(identifier)); } } diff --git a/src/main/java/moe/nea/firmament/mixins/ChatPeekScrollPatch.java b/src/main/java/moe/nea/firmament/mixins/ChatPeekScrollPatch.java new file mode 100644 index 0000000..c795908 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ChatPeekScrollPatch.java @@ -0,0 +1,27 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.MouseHandler; +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.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MouseHandler.class) +public class ChatPeekScrollPatch { + + @Inject(method = "onScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Inventory;getSelectedSlot()I"), cancellable = true) + public void onHotbarScrollWhilePeeking(long window, double horizontal, double vertical, CallbackInfo ci) { + if (Fixes.INSTANCE.shouldPeekChat() && Fixes.INSTANCE.shouldScrollPeekedChat()) ci.cancel(); + } + + @ModifyVariable(method = "onScroll", at = @At(value = "STORE"), ordinal = 0) + public int onGetChatHud(int i) { + if (Fixes.INSTANCE.shouldPeekChat() && Fixes.INSTANCE.shouldScrollPeekedChat()) + Minecraft.getInstance().gui.getChat().scrollChat(i); + return i; + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java b/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java index 9f6fb4d..2bc1374 100644 --- a/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java @@ -4,12 +4,12 @@ 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 net.minecraft.client.gui.components.ChatComponent; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyVariable; -@Mixin(ChatHud.class) +@Mixin(ChatComponent.class) public class ChatPeekingPatch { @ModifyVariable(method = "render", at = @At(value = "HEAD"), index = 5, argsOnly = true) @@ -17,7 +17,7 @@ public class ChatPeekingPatch { return old || Fixes.INSTANCE.shouldPeekChat(); } - @ModifyExpressionValue(method = "getHeight()I", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z")) + @ModifyExpressionValue(method = "getHeight()I", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent;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..9079fc9 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java @@ -0,0 +1,45 @@ +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.Minecraft; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.gui.components.ChatComponent; +import net.minecraft.client.GuiMessage; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.ChatFormatting; +import net.minecraft.util.Mth; +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(MouseButtonEvent click, boolean doubled, CallbackInfoReturnable<Boolean> cir) throws NoSuchFieldException, IllegalAccessException { + if (click.button() != 1 || !CopyChat.TConfig.INSTANCE.getCopyChat()) return; + Minecraft client = Minecraft.getInstance(); + ChatComponent chatHud = client.gui.getChat(); + int lineIndex = getChatLineIndex(chatHud, click.y()); + if (lineIndex < 0) return; + List<GuiMessage.Line> visible = ((AccessorChatHud) chatHud).getVisibleMessages_firmament(); + if (lineIndex >= visible.size()) return; + GuiMessage.Line line = visible.get(lineIndex); + String text = CopyChat.INSTANCE.orderedTextToString(line.content()); + ClipboardUtils.INSTANCE.setTextContent(text); + chatHud.addMessage(Component.literal("Copied: ").append(text).withStyle(ChatFormatting.GRAY)); + cir.setReturnValue(true); + cir.cancel(); + } + + @Unique + private int getChatLineIndex(ChatComponent chatHud, double mouseY) { + double chatLineY = ((AccessorChatHud) chatHud).toChatLineY_firmament(mouseY); + return Mth.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 index fde3580..2299068 100644 --- a/src/main/java/moe/nea/firmament/mixins/CustomDurabilityBarPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/CustomDurabilityBarPatch.java @@ -6,20 +6,20 @@ 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 net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(DrawContext.class) +@Mixin(GuiGraphics.class) public class CustomDurabilityBarPatch { @WrapOperation( - method = "drawItemBar", - at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;isItemBarVisible()Z") + method = "renderItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;isBarVisible()Z") ) private boolean onIsItemBarVisible( - ItemStack instance, Operation<Boolean> original, - @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> barOverride + ItemStack instance, Operation<Boolean> original, + @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> barOverride ) { if (original.call(instance)) return true; @@ -29,22 +29,22 @@ public class CustomDurabilityBarPatch { return barOverride.get() != null; } - @WrapOperation(method = "drawItemBar", - at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItemBarStep()I")) + @WrapOperation(method = "renderItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;getBarWidth()I")) private int overrideItemStep( - ItemStack instance, Operation<Integer> original, - @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> barOverride + ItemStack instance, Operation<Integer> original, + @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> 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")) + @WrapOperation(method = "renderItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;getBarColor()I")) private int overrideItemColor( - ItemStack instance, Operation<Integer> original, - @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> barOverride + ItemStack instance, Operation<Integer> original, + @Share("barOverride") LocalRef<DurabilityBarEvent.DurabilityBar> barOverride ) { if (barOverride.get() != null) return barOverride.get().getColor().getColor(); diff --git a/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java b/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java index 717d404..8503411 100644 --- a/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java @@ -6,8 +6,8 @@ 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 net.minecraft.util.datafix.fixes.References; +import net.minecraft.util.datafix.fixes.EntityIdFix; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -22,7 +22,7 @@ import java.util.Map; public abstract class DFUEntityIdFixPatch extends DataFix { @Shadow @Final - private static Map<String, String> RENAMED_ENTITIES; + private static Map<String, String> ID_MAP; public DFUEntityIdFixPatch(Schema outputSchema, boolean changesType) { super(outputSchema, changesType); @@ -30,6 +30,6 @@ public abstract class DFUEntityIdFixPatch extends DataFix { @Inject(method = "makeRule", at = @At("RETURN"), cancellable = true) public void onMakeRule(CallbackInfoReturnable<TypeRewriteRule> 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)))); + cir.setReturnValue(TypeRewriteRule.seq(fixTypeEverywhere("EntityIdFix", getInputSchema().findChoiceType(References.ENTITY), getOutputSchema().findChoiceType(References.ENTITY), dynamicOps -> pair -> ((Pair) pair).mapFirst(string -> ID_MAP.getOrDefault(string, (String) string))), convertUnchecked("Fix Type", getInputSchema().getType(References.ITEM_STACK), getOutputSchema().getType(References.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..3a53ab1 --- /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.renderer.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 = "bobHurt", at = @At(value = "FIELD", target = "Lnet/minecraft/world/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..ecd361d --- /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.MouseHandler; +import net.minecraft.client.player.LocalPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MouseHandler.class) +public class DispatchMouseInputEventsPatch { + @WrapWithCondition(method = "turnPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;turn(DD)V")) + public boolean onRotatePlayer(LocalPlayer 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 index e98faf6..b8460a7 100644 --- a/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java @@ -2,10 +2,10 @@ 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.server.packs.resources.ReloadableResourceManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.resources.ReloadInstance; import net.minecraft.util.Unit; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -16,10 +16,10 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -@Mixin(ReloadableResourceManagerImpl.class) +@Mixin(ReloadableResourceManager.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<Unit> initialStage, List<ResourcePack> packs, CallbackInfoReturnable<ResourceReload> cir) { + @Inject(method = "createReload", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/SimpleReloadInstance;create(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/List;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Z)Lnet/minecraft/server/packs/resources/ReloadInstance;", shift = At.Shift.BEFORE)) + public void onResourceReload(Executor prepareExecutor, Executor applyExecutor, CompletableFuture<Unit> initialStage, List<PackResources> packs, CallbackInfoReturnable<ReloadInstance> 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 index 22bebec..c286226 100644 --- a/src/main/java/moe/nea/firmament/mixins/EntityDespawnPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/EntityDespawnPatch.java @@ -3,15 +3,15 @@ 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 net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.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) +@Mixin(ClientLevel.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) { diff --git a/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java b/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java index 8ade59b..4138389 100644 --- a/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java @@ -2,32 +2,32 @@ 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 net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.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) +@Mixin(MultiPlayerGameMode.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 = "attack", at = @At("HEAD")) + private void onAttack(Player player, Entity target, CallbackInfo ci) { + EntityInteractionEvent.Companion.publish(new EntityInteractionEvent(EntityInteractionEvent.InteractionKind.ATTACK, target, InteractionHand.MAIN_HAND)); } - @Inject(method = "interactEntity", at = @At("HEAD")) - private void onInteract(PlayerEntity player, Entity entity, Hand hand, CallbackInfoReturnable<ActionResult> cir) { + @Inject(method = "interact", at = @At("HEAD")) + private void onInteract(Player player, Entity entity, InteractionHand hand, CallbackInfoReturnable<InteractionResult> 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<ActionResult> cir) { + @Inject(method = "interactAt", at = @At("HEAD")) + private void onInteractAtLocation(Player player, Entity entity, EntityHitResult hitResult, InteractionHand hand, CallbackInfoReturnable<InteractionResult> 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 index c2d6e46..0021099 100644 --- a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java +++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java @@ -3,40 +3,46 @@ 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.EntityTrackerUpdateS2CPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.client.multiplayer.CommonListenerCookie; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; 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 { +@Mixin(ClientPacketListener.class) +public abstract class EntityUpdateEventListener extends ClientCommonPacketListenerImpl { - @Shadow - private ClientWorld world; + @Shadow + private ClientLevel level; - protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { - super(client, connection, connectionState); - } + protected EntityUpdateEventListener(Minecraft client, Connection connection, CommonListenerCookie connectionState) { + super(client, connection, connectionState); + } - @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 = "handleSetEquipment", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", shift = At.Shift.AFTER)) + private void onEquipmentUpdate(ClientboundSetEquipmentPacket packet, CallbackInfo ci, @Local LivingEntity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.EquipmentUpdate(entity, packet.getSlots())); + } - @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())); - } + @Inject(method = "handleUpdateAttributes", at = @At("TAIL")) + private void onAttributeUpdate(ClientboundUpdateAttributesPacket packet, CallbackInfo ci) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( + (LivingEntity) level.getEntity(packet.getEntityId()), packet.getValues())); + } + + @Inject(method = "handleSetEntityData", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/syncher/SynchedEntityData;assignValues(Ljava/util/List;)V", shift = At.Shift.AFTER)) + private void onEntityTracker(ClientboundSetEntityDataPacket packet, CallbackInfo ci, @Local Entity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.packedItems())); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java index 699d5b7..5d8484f 100644 --- a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java @@ -3,13 +3,12 @@ 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 net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.options.controls.KeyBindsList; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.KeyMapping; +import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; @@ -19,39 +18,39 @@ 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) +@Mixin(KeyBindsList.KeyEntry.class) public class FirmKeybindsInVanillaControlsPatch { @Mutable @Shadow @Final - private ButtonWidget editButton; + private Button changeButton; @Shadow @Final - private KeyBinding binding; + private KeyMapping key; @Shadow @Final - private ButtonWidget resetButton; + private Button resetButton; - @ModifyArg(method = "<init>", 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); + @ModifyArg(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;")) + public Button.OnPress onInit(Button.OnPress action) { + var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(key); if (config == null) return action; return button -> { ((KeyBindingHandler) config.getHandler()) .getManagedConfig() - .showConfigEditor(MinecraftClient.getInstance().currentScreen); + .showConfigEditor(Minecraft.getInstance().screen); }; } - @Inject(method = "update", at = @At("HEAD"), cancellable = true) + @Inject(method = "refreshEntry", at = @At("HEAD"), cancellable = true) public void onUpdate(CallbackInfo ci) { - var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding); + var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(key); if (config == null) return; resetButton.active = false; - editButton.setMessage(Text.translatable("firmament.keybinding.external", config.getValue().format())); + changeButton.setMessage(Component.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 index c5af8b6..b2884cd 100644 --- a/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java @@ -1,29 +1,33 @@ 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 moe.nea.firmament.util.SBData; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.EffectsInInventory; 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; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(InventoryScreen.class) +@Mixin(EffectsInInventory.class) public abstract class HideStatusEffectsPatch { @Shadow - public abstract boolean shouldHideStatusEffectHud(); + public abstract boolean canSeeEffects(); - @Inject(method = "shouldHideStatusEffectHud", at = @At("HEAD"), cancellable = true) + @Inject(method = "canSeeEffects", at = @At("HEAD"), cancellable = true) private void hideStatusEffects(CallbackInfoReturnable<Boolean> cir) { - cir.setReturnValue(!Fixes.TConfig.INSTANCE.getHidePotionEffects()); + if (Fixes.TConfig.INSTANCE.getHidePotionEffects()) { + cir.setReturnValue(false); + } } - @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(); + @Inject(method = "renderEffects", at = @At("HEAD"), cancellable = true) + private void conditionalRenderStatuses(GuiGraphics context, int mouseX, int mouseY, CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHidePotionEffects()) { + ci.cancel(); + } } } diff --git a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java index 85c0462..1f7e07a 100644 --- a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java @@ -4,26 +4,34 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.events.HotbarItemRenderEvent; import moe.nea.firmament.events.HudRenderEvent; -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 moe.nea.firmament.features.fixes.Fixes; +import moe.nea.firmament.util.SBData; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.DeltaTracker; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.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) +@Mixin(Gui.class) public class HudRenderEventsPatch { @Inject(method = "renderSleepOverlay", at = @At(value = "HEAD")) - public void renderCallBack(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + public void renderCallBack(GuiGraphics context, DeltaTracker 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) { + @Inject(method = "renderSlot", at = @At("HEAD")) + public void onRenderHotbarItem(GuiGraphics context, int x, int y, DeltaTracker tickCounter, Player player, ItemStack stack, int seed, CallbackInfo ci) { if (stack != null && !stack.isEmpty()) HotbarItemRenderEvent.Companion.publish(new HotbarItemRenderEvent(stack, context, x, y, tickCounter)); } + + @Inject(method = "renderEffects", at = @At("HEAD"), cancellable = true) + public void hideStatusEffects(CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHidePotionEffectsHud() && SBData.INSTANCE.isOnSkyblock()) ci.cancel(); + } + } diff --git a/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java index a7c3875..9f338f5 100644 --- a/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java +++ b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java @@ -6,33 +6,33 @@ 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 net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; +import net.minecraft.world.phys.Vec3; 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) +@Mixin(ClientPacketListener.class) public abstract class IncomingPacketListenerPatches { - @ModifyExpressionValue(method = "onCommandTree", at = @At(value = "NEW", target = "(Lcom/mojang/brigadier/tree/RootCommandNode;)Lcom/mojang/brigadier/CommandDispatcher;", remap = false)) + @ModifyExpressionValue(method = "handleCommands", 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) { + @Inject(method = "handleParticleEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/network/PacketProcessor;)V", shift = At.Shift.AFTER), cancellable = true) + public void onParticleSpawn(ClientboundLevelParticlesPacket 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.getParticle(), + new Vec3(packet.getX(), packet.getY(), packet.getZ()), + new Vector3f(packet.getXDist(), packet.getYDist(), packet.getZDist()), + packet.alwaysShow(), packet.getCount(), - packet.getSpeed() + packet.getMaxSpeed() ); ParticleSpawnEvent.Companion.publish(event); if (event.getCancelled()) diff --git a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java index 48f3c23..ee7f570 100644 --- a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java @@ -2,18 +2,23 @@ package moe.nea.firmament.mixins; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.events.WorldKeyboardEvent; -import net.minecraft.client.Keyboard; +import moe.nea.firmament.keybindings.GenericInputAction; +import moe.nea.firmament.keybindings.InputModifiers; +import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.input.KeyEvent; +import com.mojang.blaze3d.platform.InputConstants; 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(Keyboard.class) +@Mixin(KeyboardHandler.class) public class KeyPressInWorldEventPatch { - @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V")) - public void onKeyBoardInWorld(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) { - WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(key, scancode, modifiers)); - } + @WrapWithCondition(method = "keyPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyMapping;click(Lcom/mojang/blaze3d/platform/InputConstants$Key;)V")) + public boolean onKeyBoardInWorld(InputConstants.Key key, @Local(argsOnly = true) KeyEvent keyInput) { + var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(GenericInputAction.of(keyInput), InputModifiers.of(keyInput))); + return !event.getCancelled(); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java b/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java deleted file mode 100644 index 76b34ba..0000000 --- a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java +++ /dev/null @@ -1,25 +0,0 @@ - -package moe.nea.firmament.mixins; - -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.Lifecycle; -import com.mojang.util.UndashedUuid; -import moe.nea.firmament.util.json.FirmCodecs; -import net.minecraft.component.type.ProfileComponent; -import net.minecraft.util.Uuids; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -import java.util.UUID; - -@Mixin(ProfileComponent.class) -public class LenientProfileComponentPatch { - // lambda in RecordCodecBuilder.create for BASE_CODEC - @ModifyExpressionValue(method = "method_57508", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/util/Uuids;INT_STREAM_CODEC:Lcom/mojang/serialization/Codec;")) - private static Codec<UUID> onStaticInit(Codec<UUID> original) { - return FirmCodecs.UUID_LENIENT_PREFER_INT_STREAM; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java b/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java index 0a90b35..2f82fd9 100644 --- a/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java @@ -2,8 +2,8 @@ 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 net.minecraft.client.gui.components.LogoRenderer; +import net.minecraft.client.gui.screens.TitleScreen; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -15,9 +15,9 @@ public class MainWindowFirstLoadPatch { @Unique private static boolean hasInited = false; - @Inject(method = "<init>(ZLnet/minecraft/client/gui/LogoDrawer;)V", at = @At("RETURN")) - private void onCreate(boolean doBackgroundFade, LogoDrawer logoDrawer, CallbackInfo ci) { - if (!hasInited) { + @Inject(method = "<init>(ZLnet/minecraft/client/gui/components/LogoRenderer;)V", at = @At("RETURN")) + private void onCreate(boolean doBackgroundFade, LogoRenderer logoDrawer, CallbackInfo ci) { + if (!hasInited && Firmament.INSTANCE.getDEBUG()) { try { DebugInstantiateEvent.Companion.publish(new DebugInstantiateEvent()); } catch (Throwable t) { diff --git a/src/main/java/moe/nea/firmament/mixins/MaintainKeyboardStatePatch.java b/src/main/java/moe/nea/firmament/mixins/MaintainKeyboardStatePatch.java new file mode 100644 index 0000000..97d524a --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MaintainKeyboardStatePatch.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.keybindings.FirmamentKeyboardState; +import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.input.KeyEvent; +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(KeyboardHandler.class) +public class MaintainKeyboardStatePatch { + @Inject(method = "keyPress", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/FramerateLimitTracker;onInputReceived()V")) + private void onKeyInput(long window, int action, KeyEvent input, CallbackInfo ci) { + FirmamentKeyboardState.INSTANCE.maintainState(input, action); + } +} 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..7c1189b --- /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.Minecraft; +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(Minecraft.class) +public class MinecraftInitLevelListener { + @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initBackendSystem()Lnet/minecraft/util/TimeSource$NanoTimeSource;")) + private void onInitRenderBackend(CallbackInfo ci) { + InitLevel.bump(InitLevel.RENDER_INIT); + } + + @Inject(method = "<init>", 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 = "<init>", 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 index e607ba3..bbeaf48 100644 --- a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java +++ b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java @@ -4,16 +4,20 @@ package moe.nea.firmament.mixins; 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.*; -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 moe.nea.firmament.events.HandledScreenClickEvent; +import moe.nea.firmament.keybindings.GenericInputAction; +import moe.nea.firmament.keybindings.InputModifiers; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -22,77 +26,68 @@ 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 org.spongepowered.asm.mixin.injection.callback.LocalCapture; -import java.util.Iterator; - -@Mixin(value = HandledScreen.class, priority = 990) -public abstract class MixinHandledScreen<T extends ScreenHandler> { +@Mixin(value = AbstractContainerScreen.class, priority = 990) +public abstract class MixinHandledScreen<T extends AbstractContainerMenu> { @Shadow @Final - protected T handler; + protected T menu; @Shadow - public abstract T getScreenHandler(); + public abstract T getMenu(); @Shadow - protected int y; + protected int topPos; @Shadow - protected int x; + protected int leftPos; @Unique - PlayerInventory playerInventory; + Inventory playerInventory; @Inject(method = "<init>", at = @At("TAIL")) - public void savePlayerInventory(ScreenHandler handler, PlayerInventory inventory, Text title, CallbackInfo ci) { + public void savePlayerInventory(AbstractContainerMenu handler, Inventory inventory, Component 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<Boolean> 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<Boolean> cir) { - if (HandledScreenClickEvent.Companion.publish(new HandledScreenClickEvent((HandledScreen<?>) (Object) this, mouseX, mouseY, button)).getCancelled()) { + @Inject(method = "mouseReleased", at = @At("HEAD"), cancellable = true) + private void onMouseReleased(MouseButtonEvent click, CallbackInfoReturnable<Boolean> cir) { + var self = (AbstractContainerScreen<?>) (Object) this; + var clickEvent = new HandledScreenClickEvent(self, click.x(), click.y(), click.button()); + var keyEvent = new HandledScreenKeyReleasedEvent(self, GenericInputAction.mouse(click), InputModifiers.of(click.modifiers())); + if (HandledScreenClickEvent.Companion.publish(clickEvent).getCancelled() + || HandledScreenKeyReleasedEvent.Companion.publish(keyEvent).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 = "renderContents", at = @At("HEAD")) + public void onAfterRenderForeground(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + HandledScreenForegroundEvent.Companion.publish(new HandledScreenForegroundEvent((AbstractContainerScreen<?>) (Object) this, context, mouseX, mouseY, delta)); } - @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, cursorStack)) { + @Inject(method = "slotClicked(Lnet/minecraft/world/inventory/Slot;IILnet/minecraft/world/inventory/ClickType;)V", at = @At("HEAD"), cancellable = true) + public void onMouseClickedSlot(Slot slot, int slotId, int button, ClickType actionType, CallbackInfo ci) { + if (slotId == -999 && getMenu() != null && actionType == ClickType.PICKUP) { // -999 is code for "clicked outside the main window" + ItemStack cursorStack = getMenu().getCarried(); + if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, ClickType.THROW, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE, cursorStack)) { ci.cancel(); return; } } - if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType)) { + 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)) { + if (actionType == ClickType.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<Void> original) { + @WrapOperation(method = "renderSlots", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlot(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;)V")) + public void onDrawSlots(AbstractContainerScreen instance, GuiGraphics context, Slot slot, Operation<Void> original) { var before = new SlotRenderEvents.Before(context, slot); SlotRenderEvents.Before.Companion.publish(before); original.call(instance, context, slot); 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..2210c9e --- /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.world.entity.player.Player; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.InventoryMenu; +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(InventoryMenu.class) +public class MixinPlayerScreenHandler { + + @Unique + private static final int OFF_HAND_SLOT = 40; + + @Inject(method = "<init>", at = @At("TAIL")) + private void moveOffHandSlot(Inventory inventory, boolean onServer, Player owner, CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHideOffHand()) { + InventoryMenu self = (InventoryMenu) (Object) this; + self.slots.stream() + .filter(slot -> slot.getContainerSlot() == 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..d0ec17c --- /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.screens.inventory.AbstractRecipeBookScreen; +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 = AbstractRecipeBookScreen.class, priority = 999) +public class MixinRecipeBookScreen { + @Inject(method = "initButton", 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/MousePressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/MousePressInWorldEventPatch.java new file mode 100644 index 0000000..e0ae61b --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MousePressInWorldEventPatch.java @@ -0,0 +1,22 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.WorldKeyboardEvent; +import moe.nea.firmament.keybindings.GenericInputAction; +import moe.nea.firmament.keybindings.InputModifiers; +import net.minecraft.client.MouseHandler; +import net.minecraft.client.input.MouseButtonInfo; +import com.mojang.blaze3d.platform.InputConstants; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MouseHandler.class) +public class MousePressInWorldEventPatch { + @WrapWithCondition(method = "onButton", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/KeyMapping;click(Lcom/mojang/blaze3d/platform/InputConstants$Key;)V")) + public boolean onKeyBoardInWorld(InputConstants.Key key, @Local(argsOnly = true) MouseButtonInfo input) { // TODO: handle modified mouse click instead + var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(GenericInputAction.of(input), + InputModifiers.of(input))); + return !event.getCancelled(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java b/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java index 25505b7..fd6869c 100644 --- a/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java @@ -3,16 +3,16 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.events.OutgoingPacketEvent; -import net.minecraft.client.network.ClientCommonNetworkHandler; -import net.minecraft.network.packet.Packet; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.network.protocol.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) +@Mixin(ClientCommonPacketListenerImpl.class) public class OutgoingPacketEventPatch { - @Inject(method = "sendPacket(Lnet/minecraft/network/packet/Packet;)V", at = @At("HEAD"), cancellable = true) + @Inject(method = "send(Lnet/minecraft/network/protocol/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 index 9a4626f..9ee271d 100644 --- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java @@ -3,26 +3,26 @@ 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 net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.ClickType; 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); - } +@Mixin(LocalPlayer.class) +public abstract class PlayerDropEventPatch extends Player { + public PlayerDropEventPatch() { + super(null, null); + } - @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true) - public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) { - Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0); - if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW)) { - cir.setReturnValue(false); - } - } + @Inject(method = "drop", at = @At("HEAD"), cancellable = true) + public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) { + Slot fakeSlot = new Slot(getInventory(), getInventory().getSelectedSlot(), 0, 0); + if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, ClickType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) { + cir.setReturnValue(false); + } + } } diff --git a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java deleted file mode 100644 index e7331c5..0000000 --- a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java +++ /dev/null @@ -1,36 +0,0 @@ - - -package moe.nea.firmament.mixins; - -import com.mojang.authlib.properties.Property; -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; - -import java.security.PublicKey; - -@Mixin(value = Property.class, remap = false) -public class PropertySignatureIgnorePatch { - @Inject(method = "isSignatureValid", cancellable = true, at = @At("HEAD"), remap = false) - public void onValidateSignature(PublicKey publicKey, CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } - - @Inject(method = "signature", cancellable = true, at = @At("HEAD"), remap = false) - public void returnEmptySignatureInsteadOfNull(CallbackInfoReturnable<String> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(""); - } - } - - @Inject(method = "hasSignature", cancellable = true, at = @At("HEAD"), remap = false) - public void onHasSignature(CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java index 28fe3d9..a29cdc0 100644 --- a/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java @@ -2,9 +2,9 @@ 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 net.minecraft.client.Minecraft; +import net.minecraft.client.main.GameConfig; +import net.minecraft.server.packs.resources.ReloadableResourceManager; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -12,14 +12,14 @@ 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) +@Mixin(Minecraft.class) public class ResourceReloaderRegistrationPatch { @Shadow @Final - private ReloadableResourceManagerImpl resourceManager; + private ReloadableResourceManager resourceManager; - @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourcePackManager;createResourcePacks()Ljava/util/List;", shift = At.Shift.BEFORE)) - private void onBeforeResourcePackCreation(RunArgs args, CallbackInfo ci) { + @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/repository/PackRepository;openAllSelected()Ljava/util/List;", shift = At.Shift.BEFORE)) + private void onBeforeResourcePackCreation(GameConfig 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 index fd3adca..3cfd0d6 100644 --- a/src/main/java/moe/nea/firmament/mixins/SaveCursorPositionPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/SaveCursorPositionPatch.java @@ -4,7 +4,7 @@ package moe.nea.firmament.mixins; import kotlin.Pair; import moe.nea.firmament.features.inventory.SaveCursorPosition; -import net.minecraft.client.Mouse; +import net.minecraft.client.MouseHandler; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -12,29 +12,29 @@ 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) +@Mixin(MouseHandler.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<Double, Double> cursorPosition = SaveCursorPosition.loadCursor(this.x, this.y); - if (cursorPosition == null) return; - this.x = cursorPosition.getFirst(); - this.y = cursorPosition.getSecond(); - } + @Shadow + private double xpos; + + @Shadow + private double ypos; + + @Inject(method = "grabMouse", at = @At(value = "HEAD")) + public void onLockCursor(CallbackInfo ci) { + SaveCursorPosition.saveCursorOriginal(xpos, ypos); + } + + @Inject(method = "grabMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getWindow()Lcom/mojang/blaze3d/platform/Window;", ordinal = 2)) + public void onLockCursorAfter(CallbackInfo ci) { + SaveCursorPosition.saveCursorMiddle(xpos, ypos); + } + + @Inject(method = "releaseMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getWindow()Lcom/mojang/blaze3d/platform/Window;", ordinal = 2)) + public void onUnlockCursor(CallbackInfo ci) { + Pair<Double, Double> cursorPosition = SaveCursorPosition.loadCursor(this.xpos, this.ypos); + if (cursorPosition == null) return; + this.xpos = cursorPosition.getFirst(); + this.ypos = cursorPosition.getSecond(); + } } diff --git a/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java index 2f2f188..5974277 100644 --- a/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java +++ b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java @@ -1,17 +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 net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundCommandsPacket; 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) +@Mixin(ClientPacketListener.class) public class SaveOriginalCommandTreePacket { - @Inject(method = "onCommandTree", at = @At(value = "RETURN")) - private void saveUnmodifiedCommandTree(CommandTreeS2CPacket packet, CallbackInfo ci) { + @Inject(method = "handleCommands", at = @At(value = "RETURN")) + private void saveUnmodifiedCommandTree(ClientboundCommandsPacket 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 index 6d19405..37423f0 100644 --- a/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java @@ -5,8 +5,8 @@ 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 net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -14,15 +14,15 @@ 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) +@Mixin(Minecraft.class) public abstract class ScreenChangeEventPatch { @Shadow @Nullable - public Screen currentScreen; + public Screen screen; @Inject(method = "setScreen", at = @At("HEAD"), cancellable = true) public void onScreenChange(Screen screen, CallbackInfo ci, @Local(argsOnly = true) LocalRef<Screen> screenLocalRef) { - var event = new ScreenChangeEvent(currentScreen, screen); + var event = new ScreenChangeEvent(screen, screen); if (ScreenChangeEvent.Companion.publish(event).getCancelled()) { ci.cancel(); } else if (event.getOverrideScreen() != null) { diff --git a/src/main/java/moe/nea/firmament/mixins/ScreenInputEvents.java b/src/main/java/moe/nea/firmament/mixins/ScreenInputEvents.java new file mode 100644 index 0000000..edc8fd6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ScreenInputEvents.java @@ -0,0 +1,34 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.HandledScreenKeyPressedEvent; +import moe.nea.firmament.keybindings.GenericInputAction; +import moe.nea.firmament.keybindings.InputModifiers; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +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(Screen.class) +public class ScreenInputEvents { + @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) + public void onKeyPressed(KeyEvent input, CallbackInfoReturnable<Boolean> cir) { + if (HandledScreenKeyPressedEvent.Companion.publish(new HandledScreenKeyPressedEvent( + (Screen) (Object) this, + GenericInputAction.of(input), + InputModifiers.of(input))).getCancelled()) { + cir.setReturnValue(true); + } + } + + public boolean onMouseClicked$firmament(MouseButtonEvent click, boolean doubled) { + return HandledScreenKeyPressedEvent.Companion.publish( + new HandledScreenKeyPressedEvent((Screen) (Object) this, + GenericInputAction.mouse(click), InputModifiers.current())).getCancelled(); + } + + +} diff --git a/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java index 21e7899..db87f37 100644 --- a/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java @@ -5,11 +5,11 @@ 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 net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -17,18 +17,19 @@ 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) +@Mixin(MultiPlayerGameMode.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<ItemStack> slotContent) { + @Inject(method = "handleInventoryMouseClick", at = @At(value = "FIELD", target = + "Lnet/minecraft/world/inventory/AbstractContainerMenu;slots:Lnet/minecraft/core/NonNullList;", opcode = Opcodes.GETFIELD)) + private void onSlotClickSaveSlot(int containerId, int slotId, int mouseButton, ClickType clickType, Player player, CallbackInfo ci, @Local AbstractContainerMenu handler, @Share("slotContent") LocalRef<ItemStack> slotContent) { if (0 <= slotId && slotId < handler.slots.size()) { - slotContent.set(handler.getSlot(slotId).getStack().copy()); + slotContent.set(handler.getSlot(slotId).getItem().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<ItemStack> slotContent) { + @Inject(method = "handleInventoryMouseClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V")) + private void onSlotClick(int syncId, int slotId, int button, ClickType actionType, Player player, CallbackInfo ci, @Local AbstractContainerMenu handler, @Share("slotContent") LocalRef<ItemStack> slotContent) { if (0 <= slotId && slotId < handler.slots.size()) { SlotClickEvent.Companion.publish(new SlotClickEvent( handler.getSlot(slotId), diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java index 06ecbd4..49661d0 100644 --- a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java +++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java @@ -3,51 +3,51 @@ 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 net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.client.multiplayer.CommonListenerCookie; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; 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) { +@Mixin(ClientPacketListener.class) +public abstract class SlotUpdateListener extends ClientCommonPacketListenerImpl { + protected SlotUpdateListener(Minecraft client, Connection connection, CommonListenerCookie connectionState) { super(client, connection, connectionState); } @Inject( - method = "onScreenHandlerSlotUpdate", + method = "handleContainerSetSlot", at = @At(value = "TAIL")) private void onSingleSlotUpdate( - ScreenHandlerSlotUpdateS2CPacket packet, + ClientboundContainerSetSlotPacket packet, CallbackInfo ci) { - var player = this.client.player; + var player = this.minecraft.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) { + if (packet.getContainerId() == 0) { + PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Single(packet.getSlot(), packet.getItem())); + } else if (packet.getContainerId() == player.containerMenu.containerId) { ChestInventoryUpdateEvent.Companion.publish( - new ChestInventoryUpdateEvent.Single(packet.getSlot(), packet.getStack()) + new ChestInventoryUpdateEvent.Single(packet.getSlot(), packet.getItem()) ); } } - @Inject(method = "onInventory", + @Inject(method = "handleContainerContent", at = @At("TAIL")) - private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) { - var player = this.client.player; + private void onMultiSlotUpdate(ClientboundContainerSetContentPacket packet, CallbackInfo ci) { + var player = this.minecraft.player; assert player != null; - if (packet.getSyncId() == 0) { - PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents())); - } else if (packet.getSyncId() == player.currentScreenHandler.syncId) { + if (packet.containerId() == 0) { + PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.items())); + } else if (packet.containerId() == player.containerMenu.containerId) { ChestInventoryUpdateEvent.Companion.publish( - new ChestInventoryUpdateEvent.Multi(packet.getContents()) + new ChestInventoryUpdateEvent.Multi(packet.items()) ); } } diff --git a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java index 5c52d70..e685b0c 100644 --- a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java @@ -1,30 +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.network.packet.s2c.play.PlaySoundS2CPacket; -import net.minecraft.util.math.Vec3d; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.core.Holder; +import net.minecraft.sounds.SoundSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.phys.Vec3; +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(ClientPlayNetworkHandler.class) +@Mixin(ClientPacketListener.class) public class SoundReceiveEventPatch { - @Inject(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"), cancellable = true) - private void postEventWhenSoundIsPlayed(PlaySoundS2CPacket packet, CallbackInfo ci) { + @WrapWithCondition(method = "handleSoundEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientLevel;playSeededSound(Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/Holder;Lnet/minecraft/sounds/SoundSource;FFJ)V")) + private boolean postEventWhenSoundIsPlayed(ClientLevel instance, @Nullable Entity source, double x, double y, double z, Holder<SoundEvent> sound, SoundSource category, float volume, float pitch, long seed) { var event = new SoundReceiveEvent( - packet.getSound(), - packet.getCategory(), - new Vec3d(packet.getX(), packet.getY(), packet.getZ()), - packet.getPitch(), - packet.getVolume(), - packet.getSeed() + sound, + category, + new Vec3(x,y,z), + pitch, + volume, + seed ); SoundReceiveEvent.Companion.publish(event); - if (event.getCancelled()) { - ci.cancel(); - } + return !event.getCancelled(); } } diff --git a/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java b/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java index 1acbf20..96d03bd 100644 --- a/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java @@ -3,16 +3,16 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.features.fixes.Fixes; -import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.KeyMapping; 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) +@Mixin(KeyMapping.class) public class ToggleSprintPatch { - @Inject(method = "isPressed", at = @At("HEAD"), cancellable = true) + @Inject(method = "isDown", at = @At("HEAD"), cancellable = true) public void onIsPressed(CallbackInfoReturnable<Boolean> cir) { - Fixes.INSTANCE.handleIsPressed((KeyBinding) (Object) this, cir); + Fixes.INSTANCE.handleIsPressed((KeyMapping) (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 index ac6f614..bc0ece5 100644 --- a/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java +++ b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java @@ -1,16 +1,16 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.util.mc.TolerantRegistriesOps; -import net.minecraft.registry.entry.RegistryEntryOwner; +import net.minecraft.core.HolderOwner; 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) +@Mixin(HolderOwner.class) public interface TolerateFirmamentTolerateRegistryOwners<T> { - @Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true) - private void equalTolerantRegistryOwners(RegistryEntryOwner<T> other, CallbackInfoReturnable<Boolean> cir) { + @Inject(method = "canSerializeIn", at = @At("HEAD"), cancellable = true) + private void equalTolerantRegistryOwners(HolderOwner<T> other, CallbackInfoReturnable<Boolean> 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 index d4b8c9e..70ef9cb 100644 --- a/src/main/java/moe/nea/firmament/mixins/WorldReadyEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/WorldReadyEventPatch.java @@ -3,16 +3,15 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.events.WorldReadyEvent; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.DownloadingTerrainScreen; +import net.minecraft.client.Minecraft; 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) +@Mixin(Minecraft.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)) + @Inject(method = "setLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;)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 index 847fb4d..a875bcf 100644 --- a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java @@ -2,13 +2,18 @@ package moe.nea.firmament.mixins; -import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.buffers.GpuBufferSlice; import moe.nea.firmament.events.WorldRenderLastEvent; -import net.minecraft.client.render.*; -import net.minecraft.client.util.Handle; -import net.minecraft.client.util.ObjectAllocator; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.util.profiler.Profiler; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.LevelTargetBundle; +import net.minecraft.client.renderer.RenderBuffers; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.state.LevelRenderState; +import com.mojang.blaze3d.resource.ResourceHandle; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.util.profiling.ProfilerFiller; +import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -17,31 +22,30 @@ 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) +@Mixin(LevelRenderer.class) public abstract class WorldRenderLastEventPatch { @Shadow @Final - private BufferBuilderStorage bufferBuilders; + private RenderBuffers renderBuffers; @Shadow - @Final - private DefaultFramebufferSet framebufferSet; + protected abstract void checkPoseStack(PoseStack matrices); @Shadow - protected abstract void checkEmpty(MatrixStack matrices); + private int ticks; - @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 tickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, Handle handle3, Handle handle4, boolean bl, Frustum frustum, Handle handle5, CallbackInfo ci) { - var imm = this.bufferBuilders.getEntityVertexConsumers(); - var stack = new MatrixStack(); + @Inject(method = "method_62214", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiling/ProfilerFiller;pop()V", shift = At.Shift.AFTER)) + public void onWorldRenderLast(GpuBufferSlice gpuBufferSlice, LevelRenderState worldRenderState, ProfilerFiller profiler, Matrix4f matrix4f, ResourceHandle handle, ResourceHandle handle2, boolean bl, Frustum frustum, ResourceHandle handle3, ResourceHandle handle4, CallbackInfo ci) { + var imm = this.renderBuffers.bufferSource(); + var stack = new PoseStack(); // TODO: pre-cancel this event if F1 is active var event = new WorldRenderLastEvent( - stack, tickCounter, - camera, + stack, ticks, + worldRenderState.cameraRenderState, imm ); WorldRenderLastEvent.Companion.publish(event); - imm.draw(); - checkEmpty(stack); + imm.endBatch(); + checkPoseStack(stack); } } diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java index 0a10046..cbd143d 100644 --- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java @@ -1,13 +1,13 @@ package moe.nea.firmament.mixins.accessor; -import net.minecraft.client.network.AbstractClientPlayerEntity; -import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.multiplayer.PlayerInfo; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; -@Mixin(AbstractClientPlayerEntity.class) +@Mixin(AbstractClientPlayer.class) public interface AccessorAbstractClientPlayerEntity { - @Accessor("playerListEntry") - void setPlayerListEntry_firmament(PlayerListEntry playerListEntry); + @Accessor("playerInfo") + void setPlayerListEntry_firmament(PlayerInfo 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 index 72a72f0..0b986ef 100644 --- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java @@ -1,14 +1,24 @@ package moe.nea.firmament.mixins.accessor; -import net.minecraft.client.gui.hud.ChatHud; -import net.minecraft.client.gui.hud.ChatHudLine; +import net.minecraft.client.gui.components.ChatComponent; +import net.minecraft.client.GuiMessage; 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) +@Mixin(ChatComponent.class) public interface AccessorChatHud { - @Accessor("messages") - List<ChatHudLine> getMessages_firmament(); + @Accessor("allMessages") + List<GuiMessage> getMessages_firmament(); + + @Accessor("trimmedMessages") + List<GuiMessage.Line> getVisibleMessages_firmament(); + + @Accessor("chatScrollbarPos") + int getScrolledLines_firmament(); + + @Invoker("screenToChatY") + 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 index 7ed04b1..5e0c669 100644 --- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java @@ -1,41 +1,39 @@ - - package moe.nea.firmament.mixins.accessor; -import net.minecraft.client.gui.screen.ingame.HandledScreen; -import net.minecraft.screen.slot.Slot; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; -@Mixin(HandledScreen.class) +@Mixin(AbstractContainerScreen.class) public interface AccessorHandledScreen { - @Accessor("focusedSlot") + @Accessor("hoveredSlot") @Nullable - Slot getFocusedSlot_Firmament(); + Slot getFocusedSlot_Firmament(); - @Accessor("backgroundWidth") + @Accessor("imageWidth") int getBackgroundWidth_Firmament(); - @Accessor("backgroundWidth") + @Accessor("imageWidth") void setBackgroundWidth_Firmament(int newBackgroundWidth); - @Accessor("backgroundHeight") + @Accessor("imageHeight") int getBackgroundHeight_Firmament(); - @Accessor("backgroundHeight") + @Accessor("imageHeight") void setBackgroundHeight_Firmament(int newBackgroundHeight); - @Accessor("x") + @Accessor("leftPos") int getX_Firmament(); - @Accessor("x") + @Accessor("leftPos") void setX_Firmament(int newX); - @Accessor("y") + @Accessor("topPos") int getY_Firmament(); - @Accessor("y") + @Accessor("topPos") void setY_Firmament(int newY); } diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorNbtComponent.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorNbtComponent.java new file mode 100644 index 0000000..672badf --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorNbtComponent.java @@ -0,0 +1,12 @@ +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.world.item.component.CustomData; +import net.minecraft.nbt.CompoundTag; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(CustomData.class) +public interface AccessorNbtComponent { + @Accessor("tag") + CompoundTag getUnsafeNbt_firmament(); +} 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..58c9ad9 --- /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.components.PlayerTabOverlay; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +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(PlayerTabOverlay.class) +public interface AccessorPlayerListHud { + + @Accessor("PLAYER_COMPARATOR") + static Comparator<PlayerInfo> getEntryOrdering() { + throw new AssertionError(); + } + + @Invoker("getPlayerInfos") + List<PlayerInfo> collectPlayerEntries_firmament(); + + @Accessor("footer") + @Nullable Component getFooter_firmament(); + + @Accessor("header") + @Nullable Component getHeader_firmament(); + +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorScreenHandler.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorScreenHandler.java new file mode 100644 index 0000000..0a75ea1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorScreenHandler.java @@ -0,0 +1,12 @@ +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AbstractContainerMenu.class) +public interface AccessorScreenHandler { + @Accessor("menuType") + MenuType<?> getType_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..9164af0 --- /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.renderer.LevelRenderer; +import net.minecraft.server.level.BlockDestructionProgress; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.SortedSet; + +@Mixin(LevelRenderer.class) +public interface AccessorWorldRenderer { + @Accessor("destructionProgress") + @NotNull + Long2ObjectMap<SortedSet<BlockDestructionProgress>> 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 index c705625..203b87e 100644 --- a/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java +++ b/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java @@ -2,7 +2,7 @@ package moe.nea.firmament.mixins.customgui; import moe.nea.firmament.util.customgui.CoordRememberingSlot; -import net.minecraft.screen.slot.Slot; +import net.minecraft.world.inventory.Slot; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; diff --git a/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java index 6e1090a..b0fbbe1 100644 --- a/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java +++ b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java @@ -4,17 +4,21 @@ 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.keybindings.GenericInputAction; +import moe.nea.firmament.keybindings.InputModifiers; 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 net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.network.chat.Component; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -25,19 +29,19 @@ 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<T extends ScreenHandler> extends Screen implements HasCustomGui { +@Mixin(AbstractContainerScreen.class) +public class PatchHandledScreen<T extends AbstractContainerMenu> extends Screen implements HasCustomGui { @Shadow @Final - protected T handler; + protected T menu; @Shadow - protected int x; + protected int leftPos; @Shadow - protected int y; + protected int topPos; @Shadow - protected int backgroundHeight; + protected int imageHeight; @Shadow - protected int backgroundWidth; + protected int imageWidth; @Unique public CustomGui override; @Unique @@ -47,7 +51,7 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen @Unique private int originalBackgroundHeight; - protected PatchHandledScreen(Text title) { + protected PatchHandledScreen(Component title) { super(title); } @@ -60,12 +64,12 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen @Override public void setCustomGui_Firmament(@Nullable CustomGui gui) { if (this.override != null) { - backgroundHeight = originalBackgroundHeight; - backgroundWidth = originalBackgroundWidth; + imageHeight = originalBackgroundHeight; + imageWidth = originalBackgroundWidth; } if (gui != null) { - originalBackgroundHeight = backgroundHeight; - originalBackgroundWidth = backgroundWidth; + originalBackgroundHeight = imageHeight; + originalBackgroundWidth = imageWidth; } this.override = gui; } @@ -74,14 +78,17 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen 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()) + public boolean keyReleased_firmament(KeyEvent input) { + if (HandledScreenKeyReleasedEvent.Companion.publish(new HandledScreenKeyReleasedEvent( + (AbstractContainerScreen<?>) (Object) this, + GenericInputAction.of(input), + InputModifiers.of(input))).getCancelled()) return true; - return override != null && override.keyReleased(keyCode, scanCode, modifiers); + return override != null && override.keyReleased(input); } - public boolean charTyped_firmament(char chr, int modifiers) { - return override != null && override.charTyped(chr, modifiers); + public boolean charTyped_firmament(CharacterEvent input) { + return override != null && override.charTyped(input); } @Inject(method = "init", at = @At("TAIL")) @@ -91,19 +98,19 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen } } - @Inject(method = "drawForeground", at = @At("HEAD"), cancellable = true) - private void onDrawForeground(DrawContext context, int mouseX, int mouseY, CallbackInfo ci) { + @Inject(method = "renderLabels", at = @At("HEAD"), cancellable = true) + private void onDrawForeground(GuiGraphics context, int mouseX, int mouseY, CallbackInfo ci) { if (override != null && !override.shouldDrawForeground()) ci.cancel(); } @WrapOperation( - method = "drawSlots", + method = "renderSlots", 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<Void> original) { + target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlot(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;)V")) + private void beforeSlotRender(AbstractContainerScreen instance, GuiGraphics context, Slot slot, Operation<Void> original) { if (override != null) { override.beforeSlotRender(context, slot); } @@ -113,31 +120,33 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen } } - @Inject(method = "isClickOutsideBounds", at = @At("HEAD"), cancellable = true) - public void onIsClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button, CallbackInfoReturnable<Boolean> cir) { + @Inject(method = "hasClickedOutside", at = @At("HEAD"), cancellable = true) + public void onIsClickOutsideBounds( + double mouseX, double mouseY, int left, int top, + CallbackInfoReturnable<Boolean> cir) { if (override != null) { cir.setReturnValue(override.isClickOutsideBounds(mouseX, mouseY)); } } - @Inject(method = "isPointWithinBounds", at = @At("HEAD"), cancellable = true) + @Inject(method = "isHovering(IIIIDD)Z", at = @At("HEAD"), cancellable = true) public void onIsPointWithinBounds(int x, int y, int width, int height, double pointX, double pointY, CallbackInfoReturnable<Boolean> cir) { if (override != null) { - cir.setReturnValue(override.isPointWithinBounds(x + this.x, y + this.y, width, height, pointX, pointY)); + cir.setReturnValue(override.isPointWithinBounds(x + this.leftPos, y + this.topPos, width, height, pointX, pointY)); } } - @Inject(method = "isPointOverSlot", at = @At("HEAD"), cancellable = true) + @Inject(method = "isHovering(Lnet/minecraft/world/inventory/Slot;DD)Z", at = @At("HEAD"), cancellable = true) public void onIsPointOverSlot(Slot slot, double pointX, double pointY, CallbackInfoReturnable<Boolean> cir) { if (override != null) { - cir.setReturnValue(override.isPointOverSlot(slot, this.x, this.y, pointX, pointY)); + cir.setReturnValue(override.isPointOverSlot(slot, this.leftPos, this.topPos, pointX, pointY)); } } - @Inject(method = "render", at = @At("HEAD")) - public void moveSlots(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + @Inject(method = "renderBackground", at = @At("HEAD")) + public void moveSlots(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) { if (override != null) { - for (Slot slot : handler.slots) { + for (Slot slot : menu.slots) { if (!hasRememberedSlots) { ((CoordRememberingSlot) slot).rememberCoords_firmament(); } @@ -146,7 +155,7 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen hasRememberedSlots = true; } else { if (hasRememberedSlots) { - for (Slot slot : handler.slots) { + for (Slot slot : menu.slots) { ((CoordRememberingSlot) slot).restoreCoords_firmament(); } hasRememberedSlots = false; @@ -154,7 +163,7 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen } } - @Inject(at = @At("HEAD"), method = "close", cancellable = true) + @Inject(at = @At("HEAD"), method = "onClose", cancellable = true) private void onVoluntaryExit(CallbackInfo ci) { if (override != null) { if (!override.onVoluntaryExit()) @@ -162,8 +171,8 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen } } - @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) { + @WrapWithCondition(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderBg(Lnet/minecraft/client/gui/GuiGraphics;FII)V")) + public boolean preventDrawingBackground(AbstractContainerScreen instance, GuiGraphics drawContext, float delta, int mouseX, int mouseY) { if (override != null) { override.render(drawContext, delta, mouseX, mouseY); } @@ -172,28 +181,27 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen @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<Boolean> original) { + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;mouseClicked(Lnet/minecraft/client/input/MouseButtonEvent;Z)Z")) + public boolean overrideMouseClicks(AbstractContainerScreen instance, MouseButtonEvent click, boolean doubled, Operation<Boolean> original) { if (override != null) { - if (override.mouseClick(mouseX, mouseY, button)) + if (override.mouseClick(click, doubled)) return true; } - return original.call(instance, mouseX, mouseY, button); + return original.call(instance, click, doubled); } @Inject(method = "mouseDragged", at = @At("HEAD"), cancellable = true) - public void overrideMouseDrags(double mouseX, double mouseY, int button, double deltaX, double deltaY, CallbackInfoReturnable<Boolean> cir) { + public void overrideMouseDrags(MouseButtonEvent click, double offsetX, double offsetY, CallbackInfoReturnable<Boolean> cir) { if (override != null) { - if (override.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) + if (override.mouseDragged(click, offsetX, offsetY)) cir.setReturnValue(true); } } @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) - private void overrideKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) { + private void overrideKeyPressed(KeyEvent input, CallbackInfoReturnable<Boolean> cir) { if (override != null) { - if (override.keyPressed(keyCode, scanCode, modifiers)) { + if (override.keyPressed(input)) { cir.setReturnValue(true); } } @@ -203,9 +211,9 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen @Inject( method = "mouseReleased", at = @At("HEAD"), cancellable = true) - public void overrideMouseReleases(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) { + public void overrideMouseReleases(MouseButtonEvent click, CallbackInfoReturnable<Boolean> cir) { if (override != null) { - if (override.mouseReleased(mouseX, mouseY, button)) + if (override.mouseReleased(click)) 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 index a15d825..2744fb4 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/DisableCommonPacketWarnings.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/DisableCommonPacketWarnings.java @@ -2,9 +2,9 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.client.network.ClientPlayNetworkHandler; -import net.minecraft.network.packet.CustomPayload; -import net.minecraft.util.Identifier; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -14,27 +14,27 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Objects; -@Mixin(ClientPlayNetworkHandler.class) +@Mixin(ClientPacketListener.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"))) { + @Inject(method = "handleUnknownCustomPayload", at = @At("HEAD"), cancellable = true) + public void onCustomPacketError(CustomPacketPayload customPayload, CallbackInfo ci) { + if (Objects.equals(customPayload.type(), ResourceLocation.fromNamespaceAndPath("badlion", "mods"))) { ci.cancel(); } } - @Redirect(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false)) + @Redirect(method = "handleSetEntityPassengersPacket", 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)) + @Redirect(method = "handleSetPlayerTeamPacket", 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)) + @Redirect(method = "handlePlayerInfoUpdate", 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 index 689a757..ffcfefa 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/DisableInvalidFishingHook.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/DisableInvalidFishingHook.java @@ -2,15 +2,15 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.entity.projectile.FishingBobberEntity; +import net.minecraft.world.entity.projectile.FishingHook; 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) +@Mixin(FishingHook.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)) + @Redirect(method = "recreateFromPacket", 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 index ef8c9eb..849525f 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/EarlyInstantiateTranslations.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/EarlyInstantiateTranslations.java @@ -1,19 +1,19 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.text.TranslatableTextContent; +import net.minecraft.network.chat.contents.TranslatableContents; 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) +@Mixin(TranslatableContents.class) public abstract class EarlyInstantiateTranslations { @Shadow - protected abstract void updateTranslations(); + protected abstract void decompose(); @Inject(method = "<init>", at = @At("TAIL")) private void onInit(String key, String fallback, Object[] args, CallbackInfo ci) { - updateTranslations(); + decompose(); } } diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java index 6620b47..c71f337 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java @@ -1,7 +1,7 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.client.util.Window; +import com.mojang.blaze3d.platform.Window; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java index fac0688..cc04493 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java @@ -1,16 +1,21 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Minecraft; 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) +@Mixin(Minecraft.class) public class IdentifyStopperPatch { - @Inject(method = "scheduleStop", at = @At("HEAD")) - private void onStop(CallbackInfo ci) { - Thread.dumpStack(); - } + @Shadow + private volatile boolean running; + + @Inject(method = "stop", at = @At("HEAD")) + private void onStop(CallbackInfo ci) { + if (this.running) + 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 index d7b6cc3..7d5fc80 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/MixinKeyboard.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/MixinKeyboard.java @@ -3,18 +3,18 @@ package moe.nea.firmament.mixins.devenv; import moe.nea.firmament.features.debug.DeveloperFeatures; -import net.minecraft.client.Keyboard; -import net.minecraft.client.MinecraftClient; +import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.Minecraft; 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) +@Mixin(KeyboardHandler.class) public class MixinKeyboard { - @Redirect(method = "processF3", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;reloadResources()Ljava/util/concurrent/CompletableFuture;")) - public CompletableFuture<Void> redirectReloadResources(MinecraftClient instance) { + @Redirect(method = "handleDebugKeys", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;reloadResourcePacks()Ljava/util/concurrent/CompletableFuture;")) + public CompletableFuture<Void> redirectReloadResources(Minecraft 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 index 34a733c..e39fe35 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/MixinScoreboard.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/MixinScoreboard.java @@ -2,7 +2,7 @@ package moe.nea.firmament.mixins.devenv; -import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.world.scores.Scoreboard; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -10,7 +10,7 @@ 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)) + @Redirect(method = {"addPlayerTeam", "addPlayerToTeam"}, 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 index 6d44e29..9a96df2 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java @@ -2,17 +2,17 @@ package moe.nea.firmament.mixins.devenv; import moe.nea.firmament.Firmament; -import net.minecraft.network.PacketByteBuf; -import net.minecraft.network.packet.UnknownCustomPayload; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.DiscardedPayload; 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) +@Mixin(DiscardedPayload.class) public class WarnForUnknownCustomPayloadSends { @Inject(method = "method_56493", at = @At("HEAD")) - private static void warn(UnknownCustomPayload value, PacketByteBuf buf, CallbackInfo ci) { + private static void warn(DiscardedPayload value, FriendlyByteBuf 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 index 33840c1..e513a97 100644 --- a/src/main/java/moe/nea/firmament/mixins/devenv/WarnOnMissingTranslations.java +++ b/src/main/java/moe/nea/firmament/mixins/devenv/WarnOnMissingTranslations.java @@ -2,8 +2,8 @@ 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 net.minecraft.client.resources.language.ClientLanguage; +import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -14,15 +14,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Set; import java.util.TreeSet; -@Mixin(TranslationStorage.class) +@Mixin(ClientLanguage.class) public abstract class WarnOnMissingTranslations { @Shadow - public abstract boolean hasTranslation(String key); + public abstract boolean has(String key); @Unique private final Set<String> missingTranslations = new TreeSet<>(); - @Inject(method = "get", at = @At("HEAD")) + @Inject(method = "getOrDefault", at = @At("HEAD")) private void onGetTranslationKey(String key, String fallback, CallbackInfoReturnable<String> cir) { warnForMissingTranslation(key); } @@ -30,9 +30,9 @@ public abstract class WarnOnMissingTranslations { @Unique private void warnForMissingTranslation(String key) { if (!key.contains("firmament")) return; - if (hasTranslation(key)) return; + if (has(key)) return; if (!missingTranslations.add(key)) return; - MC.INSTANCE.sendChat(Text.literal("Missing firmament translation: " + key)); + MC.INSTANCE.sendChat(Component.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..475a5bf --- /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.core.component.DataComponents; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.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 getItem(); + + @Inject(method = "isHighlightable", at = @At("HEAD"), cancellable = true) + private void dontHighlight(CallbackInfoReturnable<Boolean> cir) { + if (!Fixes.TConfig.INSTANCE.getHideSlotHighlights()) return; + var display = getItem().get(DataComponents.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..7a4a3c7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java @@ -0,0 +1,49 @@ +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.model.Model; +import net.minecraft.client.renderer.RenderType; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.layers.CapeLayer; +import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.client.renderer.entity.RenderLayerParent; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.model.PlayerModel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.world.entity.player.PlayerSkin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(CapeLayer.class) +public abstract class CustomCapeFeatureRenderer extends RenderLayer<AvatarRenderState, PlayerModel> { + public CustomCapeFeatureRenderer(RenderLayerParent<AvatarRenderState, PlayerModel> context) { + super(context); + } + +// @WrapOperation( +// method = "submit(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/client/renderer/entity/state/AvatarRenderState;FF)V", +// at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SubmitNodeCollector;submitModel(Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/RenderType;IIILnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V") +// ) +// private void onRender(SubmitNodeCollector instance, Model model, Object o, PoseStack matrixStack, RenderType renderLayer, int light, int overlay, int outlineColor, ModelFeatureRenderer.CrumblingOverlay crumblingOverlayCommand, Operation<Void> original, +// @Local(argsOnly = true) AvatarRenderState playerEntityRenderState, @Local PlayerSkin skinTextures) { +// // TODO: 1.21.10 custom capes by pre rendering the texture id. this is more viable on this version i am fairly sure, without clogging up all of the cached image render layers +// CustomCapes.render( +// playerEntityRenderState, +// vertexConsumer, +// RenderLayer.getEntitySolid(skinTextures.cape().id()), +// vertexConsumerProvider, +// matrixStack, +// updatedConsumer -> { +// original.call(instance, matrixStack, updatedConsumer, light, overlay, outlineColor); +// 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..dc933ce --- /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.renderer.entity.state.AvatarRenderState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(AvatarRenderState.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/render/IncreaseStackLimitSizeInDrawContext.java b/src/main/java/moe/nea/firmament/mixins/render/IncreaseStackLimitSizeInDrawContext.java new file mode 100644 index 0000000..2352dfa --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/IncreaseStackLimitSizeInDrawContext.java @@ -0,0 +1,20 @@ +package moe.nea.firmament.mixins.render; + +import net.minecraft.client.gui.GuiGraphics; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(GuiGraphics.class) +public class IncreaseStackLimitSizeInDrawContext { + // [22:00:57] [Render thread/ERROR] (Minecraft) Couldn't compile program for pipeline firmament:gui_textured_overlay_tris_circle: + // net.minecraft.client.gl.ShaderLoader$LoadException: Error encountered when linking program containing + // VS minecraft:core/position_tex_color and FS firmament:circle_discard_color. + // Log output: error: declarations for uniform `ColorModulator` are inside block `DynamicTransforms` and outside a block + @ModifyArg( + method = "<init>(Lnet/minecraft/client/Minecraft;Lnet/minecraft/client/gui/render/state/GuiRenderState;)V", + at = @At(value = "INVOKE", target = "Lorg/joml/Matrix3x2fStack;<init>(I)V")) + private static int increaseStackSize(int stackSize) { + return Math.max(stackSize, 48); + } +} 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..53353e2 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java @@ -0,0 +1,65 @@ +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.renderer.MultiBufferSource; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.state.CameraRenderState; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.world.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<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + @ModifyReturnValue(method = "getModelTint", 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 = "getOverlayCoords", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/OverlayTexture;u(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 = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/CameraRenderState;)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;popPose()V")) + private void afterRender(S livingEntityRenderState, PoseStack matrixStack, SubmitNodeCollector orderedRenderCommandQueue, CameraRenderState cameraRenderState, 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; + // TODO: 1.21.10 + } + + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/CameraRenderState;)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;pushPose()V")) + private void beforeRender(S livingEntityRenderState, PoseStack matrixStack, SubmitNodeCollector orderedRenderCommandQueue, CameraRenderState cameraRenderState, 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..1745fc9 --- /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.renderer.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..c84dbb6 --- /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.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.world.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<T extends Entity, S extends EntityRenderState> { + + @Inject( + method = "extractRenderState", + 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..ef8b371 --- /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.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.RenderType; +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(RenderType.OverlayStateShard.class) +public class ReplaceOverlayTexture { + @ModifyExpressionValue( + method = {"method_23555", "method_23556"}, + expect = 2, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;overlayTexture()Lnet/minecraft/client/renderer/texture/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..0cceea3 --- /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.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.layers.EquipmentLayerRenderer; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link EquipmentLayerRenderer} use a {@link RenderType} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ +@Mixin(EquipmentLayerRenderer.class) +public class UseOverlayableEquipmentRenderer { + @WrapOperation(method = "renderLayers(Lnet/minecraft/client/resources/model/EquipmentClientInfo$LayerType;Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lnet/minecraft/world/item/ItemStack;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/resources/ResourceLocation;II)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderType;armorCutoutNoCull(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/renderer/RenderType;")) + private RenderType replace(ResourceLocation texture, Operation<RenderType> original) { + if (EntityRenderTintEvent.overlayOverride != null) + return RenderType.entityTranslucent(texture); + return original.call(texture); + } + + @ModifyExpressionValue(method = "renderLayers(Lnet/minecraft/client/resources/model/EquipmentClientInfo$LayerType;Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lnet/minecraft/world/item/ItemStack;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/resources/ResourceLocation;II)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/OverlayTexture;NO_OVERLAY:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.pack(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..a867c81 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java @@ -0,0 +1,26 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.layers.CustomHeadLayer; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link CustomHeadLayer} use a {@link RenderType} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + * @see UseOverlayableItemRenderer + */ +@Mixin(CustomHeadLayer.class) +public class UseOverlayableHeadFeatureRenderer { + + @ModifyExpressionValue(method = "submit(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;FF)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/OverlayTexture;NO_OVERLAY:I", opcode = Opcodes.GETSTATIC)) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.pack(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..dcac77c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java @@ -0,0 +1,26 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.item.ItemStackRenderState; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link ItemStackRenderState} use a {@link RenderType} that allows uses Minecraft's overlay texture. + * + * @see UseOverlayableHeadFeatureRenderer + */ +@Mixin(ItemStackRenderState.LayerRenderState.class) +public class UseOverlayableItemRenderer { + @ModifyExpressionValue(method = "submit", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/item/ItemStackRenderState$LayerRenderState;renderType:Lnet/minecraft/client/renderer/RenderType;", opcode = Opcodes.GETFIELD)) + private RenderType replace(RenderType original) { + if (EntityRenderTintEvent.overlayOverride != null && original instanceof RenderType.CompositeRenderType multiPhase && multiPhase.state.textureState instanceof RenderStateShard.TextureStateShard texture && texture.cutoutTexture().isPresent()) + return RenderType.entityTranslucent(texture.cutoutTexture().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..ac102ed --- /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.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.SkullBlockRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link SkullBlockRenderer} use a {@link RenderType} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ + +@Mixin(SkullBlockRenderer.class) +public class UseOverlayableSkullBlockEntityRenderer { + @ModifyExpressionValue(method = "submitSkull(Lnet/minecraft/core/Direction;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/client/model/SkullModelBase;Lnet/minecraft/client/renderer/RenderType;ILnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/OverlayTexture;NO_OVERLAY:I")) + private static int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.pack(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/renderer/MultipleSpecialGuiRenderStates.java b/src/main/java/moe/nea/firmament/mixins/render/renderer/MultipleSpecialGuiRenderStates.java new file mode 100644 index 0000000..7c37684 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/renderer/MultipleSpecialGuiRenderStates.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 azureaaron via Skyblocker + */ + +package moe.nea.firmament.mixins.render.renderer; + +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import moe.nea.firmament.util.render.MultiSpecialGuiRenderState; +import moe.nea.firmament.util.render.MultiSpecialGuiRenderer; +import net.minecraft.client.gui.render.GuiRenderer; +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; +import net.minecraft.client.gui.render.state.GuiRenderState; +import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState; +import net.minecraft.client.renderer.MultiBufferSource; +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 java.util.HashMap; +import java.util.Map; + +/** + * The structure of this class was roughly taken from SkyBlocker, retrieved 29.07.2025 + */ +@Mixin(GuiRenderer.class) +public class MultipleSpecialGuiRenderStates { + @Shadow + @Final + private MultiBufferSource.BufferSource bufferSource; + @Shadow + @Final + GuiRenderState renderState; + @Unique + Map<MultiSpecialGuiRenderState, MultiSpecialGuiRenderer<?>> multiRenderers = new HashMap<>(); + + @Inject(method = "preparePictureInPictureState", at = @At("HEAD"), cancellable = true) + private <T extends PictureInPictureRenderState> void onPrepareElement(T elementState, int windowScaleFactor, CallbackInfo ci) { + if (elementState instanceof MultiSpecialGuiRenderState multiState) { + @SuppressWarnings({"resource", "unchecked"}) + var renderer = (PictureInPictureRenderer<T>) multiRenderers + .computeIfAbsent(multiState, elementState$ -> elementState$.createRenderer(this.bufferSource)); + renderer.prepare(elementState, renderState, windowScaleFactor); + ci.cancel(); + } + } + + @Inject(method = "close", at = @At("TAIL")) + private void onClose(CallbackInfo ci) { + multiRenderers.values().forEach(PictureInPictureRenderer::close); + } + + @Inject(method = "render(Lcom/mojang/blaze3d/buffers/GpuBufferSlice;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;clearUnusedOversizedItemRenderers()V")) + private void onAfterRender(GpuBufferSlice fogBuffer, CallbackInfo ci) { + multiRenderers.values().removeIf(it -> { + if (it.consumeRender()) { + return false; + } else { + it.close(); + return true; + } + }); + } +} diff --git a/src/main/java/moe/nea/firmament/repo/EnchantedBookCache.kt b/src/main/java/moe/nea/firmament/repo/EnchantedBookCache.kt new file mode 100644 index 0000000..0e276ce --- /dev/null +++ b/src/main/java/moe/nea/firmament/repo/EnchantedBookCache.kt @@ -0,0 +1,16 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyblockId + +class EnchantedBookCache : IReloadable { + var byName: Map<String, SkyblockId> = mapOf() + override fun reload(repo: NEURepository) { + byName = repo.items.items.values + .filter { it.displayName.endsWith("Enchanted Book") } + .associate { it.lore.first().removeColorCodes() to it.skyblockId } + } +} diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt index 7ddda9e..29c7f15 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/java/moe/nea/firmament/util/data/ManagedConfig.kt @@ -1,8 +1,8 @@ -package moe.nea.firmament.gui.config +package moe.nea.firmament.util.data 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 @@ -10,43 +10,61 @@ 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent 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.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.GuiAppender +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.gui.config.HudMetaHandler +import moe.nea.firmament.gui.config.HudPosition +import moe.nea.firmament.gui.config.IntegerHandler +import moe.nea.firmament.gui.config.KeyBindingHandler +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.ScreenUtil.setScreenLater +import moe.nea.firmament.util.ScreenUtil import moe.nea.firmament.util.collections.InstanceList +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable +import org.joml.Vector2i +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.putJsonObject +import kotlin.io.path.createDirectories +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.time.Duration +import moe.nea.firmament.gui.config.storage.ConfigStorageClass abstract class ManagedConfig( - override val name: String, + val name: String, val category: Category, - // TODO: allow vararg secondaryCategories: Category, -) : ManagedConfigElement() { +) : IDataHolder<Unit>() { 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 labelText: Component = Component.translatable("firmament.config.category.${name.lowercase()}") + val description: Component = Component.translatable("firmament.config.category.${name.lowercase()}.description") val configs: MutableList<ManagedConfig> = mutableListOf() } @@ -69,28 +87,40 @@ abstract class ManagedConfig( category.configs.add(this) } - 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()) + override fun keys(): Collection<Unit> { + return listOf(Unit) + } + + override fun clear() { + sortedOptions.forEach { + it._actualValue = null } } - fun save() { - val data = JsonObject(allOptions.mapNotNull { (key, value) -> - value.toJson()?.let { - key to it + override val storageClass: ConfigStorageClass + get() = ConfigStorageClass.CONFIG + + override fun saveTo(key: Unit): JsonObject { + return buildJsonObject { + putJsonObject(name) { + sortedOptions.forEach { + put(it.propertyName, it.toJson() ?: return@forEach) + } } - }.toMap()) - file.parent.createDirectories() - file.writeText(Firmament.json.encodeToString(data)) + } + } + + override fun explicitDefaultLoad() { + val empty = JsonObject(mapOf()) + sortedOptions.forEach { it.load(empty) } } + override fun loadFrom(key: Unit, jsonObject: JsonObject) { + val unprefixed = jsonObject[name]?.jsonObject ?: JsonObject(mapOf()) + sortedOptions.forEach { + it.load(unprefixed) + } + } val allOptions = mutableMapOf<String, ManagedOption<*>>() val sortedOptions = mutableListOf<ManagedOption<*>>() @@ -105,7 +135,6 @@ abstract class ManagedConfig( 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) } @@ -115,23 +144,27 @@ abstract class ManagedConfig( return option(propertyName, default, BooleanHandler(this)) } + protected fun colour(propertyName: String, default: () -> ChromaColour): ManagedOption<ChromaColour> { + return option(propertyName, default, ColourHandler(this)) + } + protected fun <E> choice( propertyName: String, enumClass: Class<E>, default: () -> E - ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable { + ): ManagedOption<E> where E : Enum<E>, E : StringRepresentable { return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList())) } protected inline fun <reified E> choice( propertyName: String, noinline default: () -> E - ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable { + ): ManagedOption<E> where E : Enum<E>, E : StringRepresentable { return choice(propertyName, E::class.java, default) } - private fun <E> createStringIdentifiable(x: () -> Array<out E>): Codec<E> where E : Enum<E>, E : StringIdentifiable { - return StringIdentifiable.createCodec { x() } + private fun <E> createStringIdentifiable(x: () -> Array<out E>): Codec<E> where E : Enum<E>, E : StringRepresentable { + return StringRepresentable.fromEnum { x() } } // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434 @@ -164,19 +197,21 @@ abstract class ManagedConfig( propertyName: String, width: Int, height: Int, - default: () -> Point, + default: () -> Vector2i, ): ManagedOption<HudMeta> { - val label = Text.translatable("firmament.config.${name}.${propertyName}") + val label = Component.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)) + HudMeta(HudPosition(p.x(), p.y(), 1F), Firmament.identifier(propertyName), label, width, height) + }, HudMetaHandler(this, propertyName, label, width, height)) } protected fun keyBinding( propertyName: String, default: () -> Int, - ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(default()) } + ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) { + SavedKeyBinding.Companion.keyWithoutMods(default()) + } protected fun keyBindingWithOutDefaultModifiers( propertyName: String, @@ -188,7 +223,7 @@ abstract class ManagedConfig( protected fun keyBindingWithDefaultUnbound( propertyName: String, ): ManagedOption<SavedKeyBinding> { - return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) } + return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding.Companion.unbound() } } protected fun integer( @@ -214,31 +249,36 @@ abstract class ManagedConfig( } val translationKey get() = "firmament.config.${name}" - val labelText: Text = Text.translatable(translationKey) + val labelText: Component = Component.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() + guiapp.appendFullRow( + RowComponent( + FirmButtonComponent(TextComponent("←")) { + if (parent != null) { + markDirty() + ScreenUtil.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) + val component = CenterComponent( + PanelComponent( + ScrollPanelComponent(400, 300, ColumnComponent(guiapp.panel)), + 10, + PanelComponent.DefaultBackgroundRenderer.VANILLA + ) + ) + screen = object : MoulConfigScreenComponent(Component.empty(), GuiContext(component), parent) { + override fun onClose() { + if (guiContext.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { + minecraft!!.setScreen(parent) } } } @@ -246,7 +286,7 @@ abstract class ManagedConfig( } fun showConfigEditor(parent: Screen? = null) { - setScreenLater(getConfigEditor(parent)) + ScreenUtil.setScreenLater(getConfigEditor(parent)) } } 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 index 01905c7..68789f1 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -2,14 +2,6 @@ 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 @@ -19,6 +11,8 @@ 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 @@ -33,8 +27,8 @@ 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 net.minecraft.commands.CommandBuildContext +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.commands.registerFirmamentCommand import moe.nea.firmament.events.ClientInitEvent import moe.nea.firmament.events.ClientStartedEvent @@ -44,11 +38,14 @@ 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.gui.config.storage.FirmamentConfigLoader +import moe.nea.firmament.impl.v1.FirmamentAPIImpl 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() } @@ -56,45 +53,38 @@ object 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 + allowComments = 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 - } - - - 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) - } + // Reset pretty print indent back to default to prevent getting yelled at by json + prettyPrintIndent = DEFAULT_JSON_INDENT + explicitNulls = false } val globalJob = Job() @@ -104,10 +94,10 @@ object Firmament { private fun registerCommands( dispatcher: CommandDispatcher<FabricClientCommandSource>, @Suppress("UNUSED_PARAMETER") - ctx: CommandRegistryAccess + ctx: CommandBuildContext ) { - registerFirmamentCommand(dispatcher) - CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher)) + registerFirmamentCommand(dispatcher, ctx) + CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commands)) } @JvmStatic @@ -116,15 +106,14 @@ object Firmament { @JvmStatic fun onClientInitialize() { + InitLevel.bump(InitLevel.MC_INIT) FeatureManager.subscribeEvents() - var tick = 0 + FirmamentConfigLoader.loadConfig() ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> - TickEvent.publish(TickEvent(tick++)) + 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 { @@ -135,6 +124,7 @@ object Firmament { globalJob.cancel() }) registerFirmamentEvents() + FirmamentAPIImpl.loadExtensions() ItemTooltipCallback.EVENT.register { stack, context, type, lines -> ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines)) } @@ -145,10 +135,16 @@ object Firmament { }) }) 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) + fun identifier(path: String) = ResourceLocation.fromNamespaceAndPath(MOD_ID, path) inline fun <reified T : Any> tryDecodeJsonFromStream(inputStream: InputStream): Result<T> { return runCatching { json.decodeFromStream<T>(inputStream) diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt index 789364a..f36fbb2 100644 --- a/src/main/kotlin/apis/Profiles.kt +++ b/src/main/kotlin/apis/Profiles.kt @@ -6,20 +6,20 @@ package moe.nea.firmament.apis import io.github.moulberry.repo.constants.Leveling import io.github.moulberry.repo.data.Rarity -import kotlinx.datetime.Instant +import java.time.Instant +import java.util.UUID import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlin.reflect.KProperty1 +import net.minecraft.world.item.DyeColor +import net.minecraft.ChatFormatting 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 @@ -182,13 +182,13 @@ data class PlayerData( fun getDisplayName(name: String = playerName) = rankData?.let { ("§${it.color}[${it.tag}${rankPlusDyeColor.modern}" + "${it.plus ?: ""}§${it.color}] $name") - } ?: "${Formatting.GRAY}$name" + } ?: "${ChatFormatting.GRAY}$name" } @Serializable -data class AshconNameLookup( - val username: String, - val uuid: UUID, +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 index bf55a2d..839de22 100644 --- a/src/main/kotlin/apis/Routes.kt +++ b/src/main/kotlin/apis/Routes.kt @@ -1,95 +1,54 @@ - - 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 java.util.UUID import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import kotlinx.coroutines.future.await 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.ErrorUtil import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.net.HttpUtil object Routes { - private val nameToUUID: MutableMap<String, Deferred<UUID?>> = CaseInsensitiveMap() - private val profiles: MutableMap<UUID, Deferred<Profiles?>> = mutableMapOf() - private val accounts: MutableMap<UUID, Deferred<PlayerData?>> = mutableMapOf() - private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf() - - suspend fun getPlayerNameForUUID(uuid: UUID): String? { - return withContext(MinecraftDispatcher) { - UUIDToName.computeIfAbsent(uuid) { - async(Firmament.coroutineScope.coroutineContext) { - val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid") - if (!response.status.isSuccess()) return@async null - val data = response.body<AshconNameLookup>() - launch(MinecraftDispatcher) { - nameToUUID[data.username] = async { data.uuid } - } - data.username - } - } - }.await() - } - - suspend fun getUUIDForPlayerName(name: String): UUID? { - return withContext(MinecraftDispatcher) { - nameToUUID.computeIfAbsent(name) { - async(Firmament.coroutineScope.coroutineContext) { - val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name") - if (!response.status.isSuccess()) return@async null - val data = response.body<AshconNameLookup>() - launch(MinecraftDispatcher) { - UUIDToName[data.uuid] = async { data.username } - } - data.uuid - } - } - }.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<PlayerResponse>().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<Profiles>() - } - } - }.await() - } - + private val nameToUUID: MutableMap<String, Deferred<UUID?>> = mutableMapOf() + private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf() + + suspend fun getPlayerNameForUUID(uuid: UUID): String? { + return withContext(MinecraftDispatcher) { + UUIDToName.computeIfAbsent(uuid) { + async(Firmament.coroutineScope.coroutineContext) { + val data = ErrorUtil.catch("could not get name for uuid $uuid") { + HttpUtil.request("https://mowojang.matdoes.dev/$uuid") + .forJson<MowojangNameLookup>() + .await() + }.orNull() ?: return@async null + 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 data = + ErrorUtil.catch("could not get uuid for name $name") { + HttpUtil.request("https://mowojang.matdoes.dev/$name") + .forJson<MowojangNameLookup>() + .await() + }.orNull() ?: return@async null + launch(MinecraftDispatcher) { + UUIDToName[data.id] = async { data.name } + } + data.id + } + } + }.await() + } } diff --git a/src/main/kotlin/apis/UrsaManager.kt b/src/main/kotlin/apis/UrsaManager.kt index 13f7aef..31ac36b 100644 --- a/src/main/kotlin/apis/UrsaManager.kt +++ b/src/main/kotlin/apis/UrsaManager.kt @@ -1,72 +1,80 @@ - - package moe.nea.firmament.apis -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* +import java.net.URI +import java.net.http.HttpResponse +import java.time.Duration +import java.time.Instant +import java.util.OptionalLong +import java.util.UUID import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext +import kotlinx.serialization.DeserializationStrategy +import kotlin.jvm.optionals.getOrNull +import net.minecraft.client.Minecraft import moe.nea.firmament.Firmament -import net.minecraft.client.MinecraftClient -import java.time.Duration -import java.time.Instant -import java.util.* +import moe.nea.firmament.util.net.HttpUtil 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 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) } + private var currentToken: Token? = null + private val lock = Mutex() + private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) } + + suspend fun <T> request(path: List<String>, bodyHandler: HttpResponse.BodyHandler<T>): T { + var didLock = false + try { + val host = "ursa.notenoughupdates.org" + var token = getToken(host) + if (token == null) { + lock.lock() + didLock = true + token = getToken(host) + } + var url = URI.create("https://$host") + for (segment in path) { + url = url.resolve(segment) + } + val request = HttpUtil.request(url) + if (token == null) { + withContext(Dispatchers.IO) { + val mc = Minecraft.getInstance() + val serverId = UUID.randomUUID().toString() + mc.services().sessionService.joinServer(mc.user.profileId, mc.user.accessToken, serverId) + request.header("x-ursa-username", mc.user.name) + request.header("x-ursa-serverid", serverId) + } + } else { + request.header("x-ursa-token", token.token) + } + val response = request.execute(bodyHandler) + .await() + val savedToken = response.headers().firstValue("x-ursa-token").getOrNull() + if (savedToken != null) { + val validUntil = response.headers().firstValueAsLong("x-ursa-expires").orNull()?.let { Instant.ofEpochMilli(it) } + ?: (Instant.now() + Duration.ofMinutes(55)) + currentToken = Token(validUntil, savedToken, host) + } + if (response.statusCode() != 200) { + Firmament.logger.error("Failed to contact ursa minor: ${response.statusCode()}") + } + return response.body() + } finally { + if (didLock) + lock.unlock() + } + } +} - suspend fun request(path: List<String>): 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() - } - } +private fun OptionalLong.orNull(): Long? { + if (this.isPresent)return null + return this.asLong } diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt index 42f143d..58ce5d8 100644 --- a/src/main/kotlin/commands/Duration.kt +++ b/src/main/kotlin/commands/Duration.kt @@ -7,7 +7,6 @@ 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 diff --git a/src/main/kotlin/commands/dsl.kt b/src/main/kotlin/commands/dsl.kt index d1f0d8c..4f76c69 100644 --- a/src/main/kotlin/commands/dsl.kt +++ b/src/main/kotlin/commands/dsl.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.commands import com.mojang.brigadier.arguments.ArgumentType @@ -7,14 +5,14 @@ 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 java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource 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 @@ -22,97 +20,95 @@ typealias DefaultSource = FabricClientCommandSource inline val <T : CommandContext<*>> T.context get() = this operator fun <T : Any, C : CommandContext<*>> C.get(arg: TypeSafeArg<T>): T { - return arg.get(this) + return arg.get(this) } fun literal( - name: String, - block: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>.() -> Unit + name: String, + block: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>.() -> Unit ): CaseInsensitiveLiteralCommandNode.Builder<DefaultSource> = - CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>(name).also(block) + CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>(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 - } + return when (argument) { + is Class<*> -> argument + is TypeVariable<*> -> normalizeGeneric(argument.bounds[0]) + is ParameterizedType -> normalizeGeneric(argument.rawType) + else -> Any::class.java + } } data class TypeSafeArg<T : Any>(val name: String, val argument: ArgumentType<T>) { - val argClass by lazy { - argument.javaClass - .iterate<Class<in ArgumentType<T>>> { - it.superclass - } - .flatMap { - it.genericInterfaces.toList() - } - .filterIsInstance<ParameterizedType>() - .find { it.rawType == ArgumentType::class.java }!! - .let { normalizeGeneric(it.actualTypeArguments[0]) } - } - - @JvmName("getWithThis") - fun <S> CommandContext<S>.get(): T = - get(this) - - - fun <S> get(ctx: CommandContext<S>): T { - try { - return ctx.getArgument(name, argClass) as T - } catch (e: Exception) { - if (ctx.child != null) { - return get(ctx.child) - } - throw e - } - } + val argClass by lazy { + argument.javaClass + .iterate<Class<in ArgumentType<T>>> { + it.superclass + } + .flatMap { + it.genericInterfaces.toList() + } + .filterIsInstance<ParameterizedType>() + .find { it.rawType == ArgumentType::class.java }!! + .let { normalizeGeneric(it.actualTypeArguments[0]) } + } + + @JvmName("getWithThis") + fun <S> CommandContext<S>.get(): T = + get(this) + + + fun <S> get(ctx: CommandContext<S>): T { + try { + return ctx.getArgument(name, argClass) as T + } catch (e: Exception) { + if (ctx.child != null) { + return get(ctx.child) + } + throw e + } + } } fun <T : Any> argument( - name: String, - argument: ArgumentType<T>, - block: RequiredArgumentBuilder<DefaultSource, T>.(TypeSafeArg<T>) -> Unit + name: String, + argument: ArgumentType<T>, + block: RequiredArgumentBuilder<DefaultSource, T>.(TypeSafeArg<T>) -> Unit ): RequiredArgumentBuilder<DefaultSource, T> = - RequiredArgumentBuilder.argument<DefaultSource, T>(name, argument).also { block(it, TypeSafeArg(name, argument)) } + RequiredArgumentBuilder.argument<DefaultSource, T>(name, argument).also { block(it, TypeSafeArg(name, argument)) } fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument( - name: String, - argument: ArgumentType<AT>, - block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit + name: String, + argument: ArgumentType<AT>, + block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit ): T = then(argument(name, argument, block)) fun <T : RequiredArgumentBuilder<DefaultSource, String>> T.suggestsList(provider: CommandContext<DefaultSource>.() -> Iterable<String>) { - suggests(SuggestionProvider<DefaultSource> { context, builder -> - provider(context) - .asSequence() - .filter { it.startsWith(builder.remaining, ignoreCase = true) } - .forEach { - builder.suggest(it) - } - builder.buildFuture() - }) + suggests(SuggestionProvider<DefaultSource> { context, builder -> + provider(context) + .asSequence() + .filter { it.startsWith(builder.remaining, ignoreCase = true) } + .forEach { + builder.suggest(it) + } + builder.buildFuture() + }) } fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral( - name: String, - block: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>.() -> Unit + name: String, + block: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>.() -> Unit ): T = - then(literal(name, block)) + then(literal(name, block)) fun <T : ArgumentBuilder<DefaultSource, T>> T.then(node: ArgumentBuilder<DefaultSource, *>, block: T.() -> Unit): T = - then(node).also(block) - -fun <T : ArgumentBuilder<DefaultSource, T>> T.thenExecute(block: suspend CommandContext<DefaultSource>.() -> Unit): T = - executes { - Firmament.coroutineScope.launch(MinecraftDispatcher) { - block(it) - } - 1 - } + then(node).also(block) + +fun <T : ArgumentBuilder<DefaultSource, T>> T.thenExecute(block: CommandContext<DefaultSource>.() -> Unit): T = + executes { + block(it) + 1 + } diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt index 13acb3c..727e039 100644 --- a/src/main/kotlin/commands/rome.kt +++ b/src/main/kotlin/commands/rome.kt @@ -1,23 +1,28 @@ 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 java.net.http.HttpResponse import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import kotlinx.coroutines.launch +import net.minecraft.commands.CommandBuildContext import net.minecraft.nbt.NbtOps -import net.minecraft.text.Text -import net.minecraft.text.TextCodecs +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import moe.nea.firmament.Firmament 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 @@ -32,14 +37,16 @@ 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.data.ManagedConfig import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString -fun firmamentCommand() = literal("firmament") { +fun firmamentCommand(ctx: CommandBuildContext) = literal("firmament") { thenLiteral("config") { thenExecute { AllConfigsGui.showAllGuis() @@ -64,7 +71,7 @@ fun firmamentCommand() = literal("firmament") { val configObj = ManagedConfig.allManagedConfigs.getAll().find { it.name == config } if (configObj == null) { source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.command.toggle.no-config-found", config ) @@ -74,24 +81,24 @@ fun firmamentCommand() = literal("firmament") { val propertyObj = configObj.allOptions[property] if (propertyObj == null) { source.sendFeedback( - Text.stringifiedTranslatable("firmament.command.toggle.no-property-found", property) + Component.translatableEscape("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) + Component.translatableEscape("firmament.command.toggle.not-a-toggle", property) ) return@thenExecute } propertyObj as ManagedOption<Boolean> propertyObj.value = !propertyObj.value - configObj.save() + configObj.markDirty() source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.command.toggle.toggled", configObj.labelText, propertyObj.labelText, - Text.translatable("firmament.toggle.${propertyObj.value}") + Component.translatable("firmament.toggle.${propertyObj.value}") ) ) } @@ -119,26 +126,35 @@ fun firmamentCommand() = literal("firmament") { thenLiteral("storageoverview") { thenExecute { ScreenUtil.setScreenLater(StorageOverviewScreen()) - MC.player?.networkHandler?.sendChatCommand("storage") + MC.player?.connection?.sendCommand("storage") } } thenLiteral("storage") { thenExecute { ScreenUtil.setScreenLater(StorageOverlayScreen()) - MC.player?.networkHandler?.sendChatCommand("storage") + MC.player?.connection?.sendCommand("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 + source.sendFeedback(Component.translatable("firmament.repo.reload.network")) // TODO better reporting RepoManager.launchAsyncUpdate() } } thenExecute { - source.sendFeedback(Text.translatable("firmament.repo.reload.disk")) - RepoManager.reload() + source.sendFeedback(Component.translatable("firmament.repo.reload.disk")) + Firmament.coroutineScope.launch { RepoManager.reload() } } } } @@ -147,33 +163,33 @@ fun firmamentCommand() = literal("firmament") { 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] + source.sendFeedback(Component.translatableEscape("firmament.price", itemName.neuItem)) + val bazaarData = HypixelStaticData.bazaarData[itemName.asBazaarStock] if (bazaarData != null) { - source.sendFeedback(Text.translatable("firmament.price.bazaar")) + source.sendFeedback(Component.translatable("firmament.price.bazaar")) source.sendFeedback( - Text.stringifiedTranslatable("firmament.price.bazaar.productid", bazaarData.productId.bazaarId) + Component.translatableEscape("firmament.price.bazaar.productid", bazaarData.productId.bazaarId) ) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.price.bazaar.buy.price", FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1) ) ) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.price.bazaar.buy.order", bazaarData.quickStatus.buyOrders ) ) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.price.bazaar.sell.price", FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1) ) ) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.price.bazaar.sell.order", bazaarData.quickStatus.sellOrders ) @@ -182,7 +198,7 @@ fun firmamentCommand() = literal("firmament") { val lowestBin = HypixelStaticData.lowestBin[itemName] if (lowestBin != null) { source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.price.lowestbin", FirmFormatters.formatCommas(lowestBin, 1) ) @@ -191,11 +207,11 @@ fun firmamentCommand() = literal("firmament") { } } } - thenLiteral("dev") { + thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { thenLiteral("simulate") { thenArgument("message", RestArgumentType) { message -> thenExecute { - MC.instance.messageHandler.onGameMessage(Text.literal(get(message)), false) + MC.instance.chatListener.handleSystemMessage(Component.literal(get(message)), false) } } } @@ -208,28 +224,46 @@ fun firmamentCommand() = literal("firmament") { val enabled = DebugLogger.EnabledLogs.data if (tagText in enabled) { enabled.remove(tagText) - source.sendFeedback(Text.literal("Disabled $tagText debug logging")) + source.sendFeedback(Component.literal("Disabled $tagText debug logging")) } else { enabled.add(tagText) - source.sendFeedback(Text.literal("Enabled $tagText debug logging")) + source.sendFeedback(Component.literal("Enabled $tagText debug logging")) } } } } } + thenLiteral("screens") { + thenExecute { + MC.sendChat( + Component.literal( + """ + |Screen: ${MC.screen} (${MC.screen?.title}) + |Screen Handler: ${MC.handledScreen?.menu} ${MC.handledScreen?.menu?.containerId} + |Player Screen Handler: ${MC.player?.containerMenu} ${MC.player?.containerMenu?.containerId} + """.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 + MC.inGameHud.chat.messages.forEach { + val nbt = ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow println(nbt) } } thenArgument("search", string()) { search -> thenExecute { - MC.inGameHud.chatHud.messages + MC.inGameHud.chat.messages .filter { this[search] in it.content.unformattedString } .forEach { - val nbt = TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow + val nbt = ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow println(SNbtFormatter.prettify(nbt)) } } @@ -237,31 +271,40 @@ fun firmamentCommand() = literal("firmament") { } thenLiteral("sbdata") { thenExecute { - source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.profile", SBData.profileId)) + source.sendFeedback(Component.translatableEscape("firmament.sbinfo.profile", SBData.profileId)) val locrawInfo = SBData.locraw if (locrawInfo == null) { - source.sendFeedback(Text.translatable("firmament.sbinfo.nolocraw")) + source.sendFeedback(Component.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(Component.translatableEscape("firmament.sbinfo.server", locrawInfo.server)) + source.sendFeedback(Component.translatableEscape("firmament.sbinfo.gametype", locrawInfo.gametype)) + source.sendFeedback(Component.translatableEscape("firmament.sbinfo.mode", locrawInfo.mode)) + source.sendFeedback(Component.translatableEscape("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)) + player.level.getEntities(player, player.boundingBox.inflate(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)) + Firmament.coroutineScope.launch { + source.sendFeedback(Component.translatable("firmament.ursa.debugrequest.start")) + val text = UrsaManager.request(get(path).split("/"), HttpResponse.BodyHandlers.ofString()) + source.sendFeedback(Component.translatableEscape("firmament.ursa.debugrequest.result", text)) + } } } } @@ -270,70 +313,112 @@ fun firmamentCommand() = literal("firmament") { 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:")) + source.sendFeedback( + tr( + "firmament.event.bustype", + "- $prefixName:" + ) + ) eventBus.handlers.forEach { handler -> - source.sendFeedback(tr( - "firmament.event.handler", - " * ${handler.label}")) + source.sendFeedback( + tr( + "firmament.event.handler", + " * ${handler.label}" + ) + ) } } } } thenLiteral("caches") { thenExecute { - source.sendFeedback(Text.literal("Caches:")) + source.sendFeedback(Component.literal("Caches:")) WeakCache.allInstances.getAll().forEach { - source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}")) + source.sendFeedback(Component.literal(" - ${it.name}: ${it.size}")) } - source.sendFeedback(Text.translatable("Instance lists:")) + source.sendFeedback(Component.translatable("Instance lists:")) InstanceList.allInstances.getAll().forEach { - source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}")) + source.sendFeedback(Component.literal(" - ${it.name}: ${it.size}")) } } } thenLiteral("mixins") { thenExecute { - source.sendFeedback(Text.translatable("firmament.mixins.start")) - MixinPlugin.appliedMixins - .map { it.removePrefix(MixinPlugin.mixinPackage) } - .forEach { - source.sendFeedback(Text.literal(" - ").withColor(0xD020F0) - .append(Text.literal(it).withColor(0xF6BA20))) - } + 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( + Component.literal(" - ").withColor(0xD020F0) + .append(Component.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"))}")) + 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.overlays", + "Overlays: ${RepoManager.overlayData.overlays.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)) + CommandEvent.SubCommand.publish(CommandEvent.SubCommand(this@literal, ctx)) } -fun registerFirmamentCommand(dispatcher: CommandDispatcher<FabricClientCommandSource>) { - val firmament = dispatcher.register(firmamentCommand()) +fun registerFirmamentCommand(dispatcher: CommandDispatcher<FabricClientCommandSource>, ctx: CommandBuildContext) { + val firmament = dispatcher.register(firmamentCommand(ctx)) dispatcher.register(literal("firm") { redirect(firmament) }) diff --git a/src/main/kotlin/events/AllowChatEvent.kt b/src/main/kotlin/events/AllowChatEvent.kt index 3069843..86395c9 100644 --- a/src/main/kotlin/events/AllowChatEvent.kt +++ b/src/main/kotlin/events/AllowChatEvent.kt @@ -2,14 +2,14 @@ package moe.nea.firmament.events +import net.minecraft.network.chat.Component 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() { +data class AllowChatEvent(val text: Component) : FirmamentEvent.Cancellable() { val unformattedString = text.unformattedString companion object : FirmamentEventBus<AllowChatEvent>() diff --git a/src/main/kotlin/events/AttackBlockEvent.kt b/src/main/kotlin/events/AttackBlockEvent.kt index bbaa81d..81a2952 100644 --- a/src/main/kotlin/events/AttackBlockEvent.kt +++ b/src/main/kotlin/events/AttackBlockEvent.kt @@ -1,16 +1,16 @@ 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 +import net.minecraft.world.entity.player.Player +import net.minecraft.world.InteractionHand +import net.minecraft.core.BlockPos +import net.minecraft.core.Direction +import net.minecraft.world.level.Level data class AttackBlockEvent( - val player: PlayerEntity, - val world: World, - val hand: Hand, + val player: Player, + val world: Level, + val hand: InteractionHand, val blockPos: BlockPos, val direction: Direction ) : FirmamentEvent.Cancellable() { diff --git a/src/main/kotlin/events/ChestInventoryUpdateEvent.kt b/src/main/kotlin/events/ChestInventoryUpdateEvent.kt index ddf54fc..e3acd12 100644 --- a/src/main/kotlin/events/ChestInventoryUpdateEvent.kt +++ b/src/main/kotlin/events/ChestInventoryUpdateEvent.kt @@ -1,6 +1,6 @@ package moe.nea.firmament.events -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack import moe.nea.firmament.util.MC sealed class ChestInventoryUpdateEvent : FirmamentEvent() { diff --git a/src/main/kotlin/events/CommandEvent.kt b/src/main/kotlin/events/CommandEvent.kt index cc9cf45..bdd63ca 100644 --- a/src/main/kotlin/events/CommandEvent.kt +++ b/src/main/kotlin/events/CommandEvent.kt @@ -4,7 +4,7 @@ package moe.nea.firmament.events import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.tree.LiteralCommandNode -import net.minecraft.command.CommandRegistryAccess +import net.minecraft.commands.CommandBuildContext import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode import moe.nea.firmament.commands.DefaultSource import moe.nea.firmament.commands.literal @@ -12,7 +12,7 @@ import moe.nea.firmament.commands.thenLiteral data class CommandEvent( val dispatcher: CommandDispatcher<DefaultSource>, - val ctx: CommandRegistryAccess, + val ctx: CommandBuildContext, val serverCommands: CommandDispatcher<*>?, ) : FirmamentEvent() { companion object : FirmamentEventBus<CommandEvent>() @@ -23,6 +23,7 @@ data class CommandEvent( */ data class SubCommand( val builder: CaseInsensitiveLiteralCommandNode.Builder<DefaultSource>, + val commandRegistryAccess: CommandBuildContext, ) : FirmamentEvent() { companion object : FirmamentEventBus<SubCommand>() diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt index 11528fd..3d96b34 100644 --- a/src/main/kotlin/events/CustomItemModelEvent.kt +++ b/src/main/kotlin/events/CustomItemModelEvent.kt @@ -1,23 +1,75 @@ package moe.nea.firmament.events -import net.minecraft.item.ItemStack -import net.minecraft.util.Identifier +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation +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, - var overrideModel: Identifier? = null, + val itemStack: ItemStack, + val itemModelManager: IntrospectableItemModelManager, + var overrideModel: ResourceLocation? = null, ) : FirmamentEvent() { companion object : FirmamentEventBus<CustomItemModelEvent>() { + val weakCache = + object : WeakCache<ItemStack, IntrospectableItemModelManager, Optional<ResourceLocation>>("ItemModelIdentifier") { + override fun mkRef( + key: ItemStack, + extraData: IntrospectableItemModelManager + ): WeakCache<ItemStack, IntrospectableItemModelManager, Optional<ResourceLocation>>.Ref { + return IRef(key, extraData) + } + + inner class IRef(weakInstance: ItemStack, data: IntrospectableItemModelManager) : + Ref(weakInstance, data) { + override fun shouldBeEvicted(): Boolean = false + val isSimpleStack = weakInstance.componentsPatch.isEmpty || (weakInstance.componentsPatch.size() == 1 && weakInstance.get( + DataComponents.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?): Identifier? { - // TODO: Re-add memoization and add an error / warning if the model does not exist + fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): ResourceLocation? { if (itemStack == null) return null - return publish(CustomItemModelEvent(itemStack)).overrideModel + return cache.invoke(itemStack, itemModelManager).getOrNull() } + + fun getModelIdentifier0( + itemStack: ItemStack, + itemModelManager: IntrospectableItemModelManager + ): Optional<ResourceLocation> { + // TODO: add an error / warning if the model does not exist + return Optional.ofNullable(publish(CustomItemModelEvent(itemStack, itemModelManager)).overrideModel) + } + } + + fun overrideIfExists(overrideModel: ResourceLocation) { + if (itemModelManager.hasModel_firmament(overrideModel)) + this.overrideModel = overrideModel } - fun overrideIfExists(overrideModel: Identifier) { - this.overrideModel = overrideModel + fun overrideIfEmpty(identifier: ResourceLocation) { + if (overrideModel == null) + overrideModel = identifier } } diff --git a/src/main/kotlin/events/EarlyResourceReloadEvent.kt b/src/main/kotlin/events/EarlyResourceReloadEvent.kt index ec8377a..b9cf717 100644 --- a/src/main/kotlin/events/EarlyResourceReloadEvent.kt +++ b/src/main/kotlin/events/EarlyResourceReloadEvent.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.events import java.util.concurrent.Executor -import net.minecraft.resource.ResourceManager +import net.minecraft.server.packs.resources.ResourceManager data class EarlyResourceReloadEvent(val resourceManager: ResourceManager, val preparationExecutor: Executor) : FirmamentEvent() { diff --git a/src/main/kotlin/events/EntityDespawnEvent.kt b/src/main/kotlin/events/EntityDespawnEvent.kt index 93dc477..d007f96 100644 --- a/src/main/kotlin/events/EntityDespawnEvent.kt +++ b/src/main/kotlin/events/EntityDespawnEvent.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.events -import net.minecraft.entity.Entity +import net.minecraft.world.entity.Entity data class EntityDespawnEvent( val entity: Entity?, val entityId: Int, diff --git a/src/main/kotlin/events/EntityInteractionEvent.kt b/src/main/kotlin/events/EntityInteractionEvent.kt index 123ea39..8285e1b 100644 --- a/src/main/kotlin/events/EntityInteractionEvent.kt +++ b/src/main/kotlin/events/EntityInteractionEvent.kt @@ -1,13 +1,13 @@ package moe.nea.firmament.events -import net.minecraft.entity.Entity -import net.minecraft.util.Hand +import net.minecraft.world.entity.Entity +import net.minecraft.world.InteractionHand data class EntityInteractionEvent( val kind: InteractionKind, val entity: Entity, - val hand: Hand, + val hand: InteractionHand, ) : FirmamentEvent() { companion object : FirmamentEventBus<EntityInteractionEvent>() enum class InteractionKind { diff --git a/src/main/kotlin/events/EntityRenderTintEvent.kt b/src/main/kotlin/events/EntityRenderTintEvent.kt new file mode 100644 index 0000000..63528bc --- /dev/null +++ b/src/main/kotlin/events/EntityRenderTintEvent.kt @@ -0,0 +1,67 @@ +package moe.nea.firmament.events + +import net.minecraft.client.renderer.GameRenderer +import net.minecraft.client.renderer.texture.OverlayTexture +import net.minecraft.client.renderer.entity.state.EntityRenderState +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.events.EntityRenderTintEvent.Companion.overlayOverride +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<EntityRenderTintEvent>() { + /** + * 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 index d091984..9f5a101 100644 --- a/src/main/kotlin/events/EntityUpdateEvent.kt +++ b/src/main/kotlin/events/EntityUpdateEvent.kt @@ -1,10 +1,14 @@ - package moe.nea.firmament.events -import net.minecraft.entity.Entity -import net.minecraft.entity.LivingEntity -import net.minecraft.entity.data.DataTracker -import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket +import com.mojang.datafixers.util.Pair +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.world.item.ItemStack +import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.util.MC /** * This event is fired when some entity properties are updated. @@ -13,19 +17,44 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket * *after* the values have been applied to the entity. */ sealed class EntityUpdateEvent : FirmamentEvent() { - companion object : FirmamentEventBus<EntityUpdateEvent>() + companion object : FirmamentEventBus<EntityUpdateEvent>() { + @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.inventoryMenu.findSlot(p.inventory, stackIndex).asInt + event.getOrNull(slotIndex)?.let { + Pair.of(slot, it) + } + } + if (updatedSlots.isNotEmpty()) + publish(EquipmentUpdate(p, updatedSlots)) + } + } - abstract val entity: Entity + abstract val entity: Entity - data class AttributeUpdate( + data class AttributeUpdate( override val entity: LivingEntity, - val attributes: List<EntityAttributesS2CPacket.Entry>, - ) : EntityUpdateEvent() + val attributes: List<ClientboundUpdateAttributesPacket.AttributeSnapshot>, + ) : EntityUpdateEvent() + + data class TrackedDataUpdate( + override val entity: Entity, + val trackedValues: List<SynchedEntityData.DataValue<*>>, + ) : EntityUpdateEvent() - data class TrackedDataUpdate( + data class EquipmentUpdate( override val entity: Entity, - val trackedValues: List<DataTracker.SerializedEntry<*>>, - ) : EntityUpdateEvent() + val newEquipment: List<Pair<EquipmentSlot, ItemStack>>, + ) : EntityUpdateEvent() -// TODO: onEntityPassengersSet, onEntityAttach?, onEntityEquipmentUpdate, onEntityStatusEffect +// TODO: onEntityPassengersSet, onEntityAttach?, onEntityStatusEffect } diff --git a/src/main/kotlin/events/FeaturesInitializedEvent.kt b/src/main/kotlin/events/FeaturesInitializedEvent.kt deleted file mode 100644 index ad2ad8a..0000000 --- a/src/main/kotlin/events/FeaturesInitializedEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ - -package moe.nea.firmament.events - -import moe.nea.firmament.features.FirmamentFeature - -data class FeaturesInitializedEvent(val features: List<FirmamentFeature>) : FirmamentEvent() { - companion object : FirmamentEventBus<FeaturesInitializedEvent>() -} diff --git a/src/main/kotlin/events/FinalizeResourceManagerEvent.kt b/src/main/kotlin/events/FinalizeResourceManagerEvent.kt index 12167f8..72fa9c4 100644 --- a/src/main/kotlin/events/FinalizeResourceManagerEvent.kt +++ b/src/main/kotlin/events/FinalizeResourceManagerEvent.kt @@ -2,25 +2,25 @@ 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 +import net.minecraft.server.packs.resources.ReloadableResourceManager +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.PreparableReloadListener data class FinalizeResourceManagerEvent( - val resourceManager: ReloadableResourceManagerImpl, + val resourceManager: ReloadableResourceManager, ) : FirmamentEvent() { companion object : FirmamentEventBus<FinalizeResourceManagerEvent>() inline fun registerOnApply(name: String, crossinline function: () -> Unit) { - resourceManager.registerReloader(object : ResourceReloader { + resourceManager.registerReloadListener(object : PreparableReloadListener { override fun reload( - synchronizer: ResourceReloader.Synchronizer, - manager: ResourceManager, - prepareExecutor: Executor, - applyExecutor: Executor + store: PreparableReloadListener.SharedState, + prepareExecutor: Executor, + reloadSynchronizer: PreparableReloadListener.PreparationBarrier, + applyExecutor: Executor ): CompletableFuture<Void> { return CompletableFuture.completedFuture(Unit) - .thenCompose(synchronizer::whenPrepared) + .thenCompose(reloadSynchronizer::wait) .thenAcceptAsync({ function() }, applyExecutor) } diff --git a/src/main/kotlin/events/FirmamentEventBus.kt b/src/main/kotlin/events/FirmamentEventBus.kt index 71331d1..af4e16a 100644 --- a/src/main/kotlin/events/FirmamentEventBus.kt +++ b/src/main/kotlin/events/FirmamentEventBus.kt @@ -3,6 +3,7 @@ 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 /** @@ -48,7 +49,7 @@ open class FirmamentEventBus<T : FirmamentEvent> { val klass = e.javaClass if (!function.knownErrors.contains(klass) || Firmament.DEBUG) { function.knownErrors.add(klass) - Firmament.logger.error("Caught exception during processing event $event by $function", e) + ErrorUtil.softError("Caught exception during processing event $event by $function", e) } } } diff --git a/src/main/kotlin/events/HandledScreenClickEvent.kt b/src/main/kotlin/events/HandledScreenClickEvent.kt index 4c3003c..c9890af 100644 --- a/src/main/kotlin/events/HandledScreenClickEvent.kt +++ b/src/main/kotlin/events/HandledScreenClickEvent.kt @@ -1,10 +1,11 @@ - - package moe.nea.firmament.events -import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen -data class HandledScreenClickEvent(val screen: HandledScreen<*>, val mouseX: Double, val mouseY: Double, val button: Int) : - FirmamentEvent.Cancellable() { - companion object : FirmamentEventBus<HandledScreenClickEvent>() +data class HandledScreenClickEvent( + val screen: AbstractContainerScreen<*>, + val mouseX: Double, val mouseY: Double, val button: Int +) : + FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus<HandledScreenClickEvent>() } diff --git a/src/main/kotlin/events/HandledScreenForegroundEvent.kt b/src/main/kotlin/events/HandledScreenForegroundEvent.kt index f16d30e..3a3d7f6 100644 --- a/src/main/kotlin/events/HandledScreenForegroundEvent.kt +++ b/src/main/kotlin/events/HandledScreenForegroundEvent.kt @@ -2,12 +2,12 @@ package moe.nea.firmament.events -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen data class HandledScreenForegroundEvent( - val screen: HandledScreen<*>, - val context: DrawContext, + val screen: AbstractContainerScreen<*>, + val context: GuiGraphics, val mouseX: Int, val mouseY: Int, val delta: Float diff --git a/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt index 183ec71..5a5f1e9 100644 --- a/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt +++ b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt @@ -1,38 +1,40 @@ 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 +import org.lwjgl.glfw.GLFW +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import moe.nea.firmament.keybindings.GenericInputAction +import moe.nea.firmament.keybindings.InputModifiers +import moe.nea.firmament.keybindings.SavedKeyBinding -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) - } +sealed interface HandledScreenInputEvent { + val screen: Screen + val input: GenericInputAction + val modifiers: InputModifiers } data class HandledScreenKeyPressedEvent( - override val screen: HandledScreen<*>, - override val keyCode: Int, - override val scanCode: Int, - override val modifiers: Int -) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent { + override val screen: Screen, + override val input: GenericInputAction, + override val modifiers: InputModifiers, + // TODO: val isRepeat: Boolean, +) : FirmamentEvent.Cancellable(), HandledScreenInputEvent { + fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean { + return keyBinding.matches(input, modifiers, atLeast) + } + + fun isLeftClick() = input == GenericInputAction.mouse(GLFW.GLFW_MOUSE_BUTTON_LEFT) companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>() } data class HandledScreenKeyReleasedEvent( - override val screen: HandledScreen<*>, - override val keyCode: Int, - override val scanCode: Int, - override val modifiers: Int -) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent { + override val screen: AbstractContainerScreen<*>, + override val input: GenericInputAction, + override val modifiers: InputModifiers, +) : FirmamentEvent.Cancellable(), HandledScreenInputEvent { + fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean { + return keyBinding.matches(input, modifiers, atLeast) + } + companion object : FirmamentEventBus<HandledScreenKeyReleasedEvent>() } diff --git a/src/main/kotlin/events/HandledScreenPushREIEvent.kt b/src/main/kotlin/events/HandledScreenPushREIEvent.kt index 1bb495a..e2b5a40 100644 --- a/src/main/kotlin/events/HandledScreenPushREIEvent.kt +++ b/src/main/kotlin/events/HandledScreenPushREIEvent.kt @@ -3,10 +3,10 @@ package moe.nea.firmament.events import me.shedaniel.math.Rectangle -import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen data class HandledScreenPushREIEvent( - val screen: HandledScreen<*>, + val screen: AbstractContainerScreen<*>, val rectangles: MutableList<Rectangle> = mutableListOf() ) : FirmamentEvent() { diff --git a/src/main/kotlin/events/HotbarItemRenderEvent.kt b/src/main/kotlin/events/HotbarItemRenderEvent.kt index a1940e6..0d4e75b 100644 --- a/src/main/kotlin/events/HotbarItemRenderEvent.kt +++ b/src/main/kotlin/events/HotbarItemRenderEvent.kt @@ -2,16 +2,16 @@ package moe.nea.firmament.events -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderTickCounter -import net.minecraft.item.ItemStack +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.DeltaTracker +import net.minecraft.world.item.ItemStack data class HotbarItemRenderEvent( - val item: ItemStack, - val context: DrawContext, - val x: Int, - val y: Int, - val tickDelta: RenderTickCounter, + val item: ItemStack, + val context: GuiGraphics, + val x: Int, + val y: Int, + val tickDelta: DeltaTracker, ) : FirmamentEvent() { companion object : FirmamentEventBus<HotbarItemRenderEvent>() } diff --git a/src/main/kotlin/events/HudRenderEvent.kt b/src/main/kotlin/events/HudRenderEvent.kt index a773a93..a397378 100644 --- a/src/main/kotlin/events/HudRenderEvent.kt +++ b/src/main/kotlin/events/HudRenderEvent.kt @@ -1,17 +1,21 @@ - - package moe.nea.firmament.events -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderTickCounter -import net.minecraft.world.GameMode +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.DeltaTracker +import net.minecraft.world.level.GameType 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<HudRenderEvent>() +data class HudRenderEvent(val context: GuiGraphics, val tickDelta: DeltaTracker) : FirmamentEvent.Cancellable() { + val isRenderingHud = !MC.options.hideGui + val isRenderingCursor = MC.interactionManager?.playerMode != GameType.SPECTATOR && isRenderingHud + + init { + if (!isRenderingHud) + cancel() + } + + companion object : FirmamentEventBus<HudRenderEvent>() } diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt index cd2b676..bda1bb3 100644 --- a/src/main/kotlin/events/IsSlotProtectedEvent.kt +++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt @@ -1,46 +1,64 @@ - - 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 net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType 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, + val actionType: ClickType, var isProtected: Boolean, val itemStackOverride: ItemStack?, + val origin: MoveOrigin, var silent: Boolean = false, ) : FirmamentEvent() { - val itemStack get() = itemStackOverride ?: slot!!.stack + val itemStack get() = itemStackOverride ?: slot!!.item + + fun protect() { + isProtected = true + silent = false + } - fun protect() { - isProtected = true - } + fun protectSilent() { + if (!isProtected) { + silent = true + } + isProtected = true + } - fun protectSilent() { - if (!isProtected) { - silent = true - } - isProtected = true - } + enum class MoveOrigin { + DROP_FROM_HOTBAR, + SALVAGE, + INVENTORY_MOVE + ; + } - companion object : FirmamentEventBus<IsSlotProtectedEvent>() { - @JvmStatic - @JvmOverloads - fun shouldBlockInteraction(slot: Slot?, action: SlotActionType, itemStackOverride: ItemStack? = null): Boolean { - if (slot == null && itemStackOverride == null) return false - val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride) - publish(event) - if (event.isProtected && !event.silent) { - MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name)) - CommonSoundEffects.playFailure() - } - return event.isProtected - } - } + companion object : FirmamentEventBus<IsSlotProtectedEvent>() { + @JvmStatic + @JvmOverloads + fun shouldBlockInteraction( + slot: Slot?, action: ClickType, + 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.hoverName}.\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 index d86e06f..9acd9bf 100644 --- a/src/main/kotlin/events/ItemTooltipEvent.kt +++ b/src/main/kotlin/events/ItemTooltipEvent.kt @@ -2,13 +2,13 @@ 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 +import net.minecraft.world.item.Item.TooltipContext +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.TooltipFlag +import net.minecraft.network.chat.Component data class ItemTooltipEvent( - val stack: ItemStack, val context: TooltipContext, val type: TooltipType, val lines: MutableList<Text> + val stack: ItemStack, val context: TooltipContext, val type: TooltipFlag, val lines: MutableList<Component> ) : FirmamentEvent() { companion object : FirmamentEventBus<ItemTooltipEvent>() } diff --git a/src/main/kotlin/events/JoinServerEvent.kt b/src/main/kotlin/events/JoinServerEvent.kt new file mode 100644 index 0000000..6fafbd7 --- /dev/null +++ b/src/main/kotlin/events/JoinServerEvent.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.events + +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.minecraft.client.multiplayer.ClientPacketListener + +data class JoinServerEvent( + val networkHandler: ClientPacketListener, + val packetSender: PacketSender, +) : FirmamentEvent() { + companion object : FirmamentEventBus<JoinServerEvent>() +} diff --git a/src/main/kotlin/events/ModifyChatEvent.kt b/src/main/kotlin/events/ModifyChatEvent.kt index a5868e8..5432490 100644 --- a/src/main/kotlin/events/ModifyChatEvent.kt +++ b/src/main/kotlin/events/ModifyChatEvent.kt @@ -2,16 +2,16 @@ package moe.nea.firmament.events +import net.minecraft.network.chat.Component 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() { +data class ModifyChatEvent(val originalText: Component) : FirmamentEvent() { var unformattedString = originalText.unformattedString private set - var replaceWith: Text = originalText + var replaceWith: Component = originalText set(value) { field = value unformattedString = value.unformattedString diff --git a/src/main/kotlin/events/OutgoingPacketEvent.kt b/src/main/kotlin/events/OutgoingPacketEvent.kt index 93890ea..0385647 100644 --- a/src/main/kotlin/events/OutgoingPacketEvent.kt +++ b/src/main/kotlin/events/OutgoingPacketEvent.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.events -import net.minecraft.network.packet.Packet +import net.minecraft.network.protocol.Packet data class OutgoingPacketEvent(val packet: Packet<*>) : FirmamentEvent.Cancellable() { companion object : FirmamentEventBus<OutgoingPacketEvent>() diff --git a/src/main/kotlin/events/ParticleSpawnEvent.kt b/src/main/kotlin/events/ParticleSpawnEvent.kt index 9359e4b..578fa0d 100644 --- a/src/main/kotlin/events/ParticleSpawnEvent.kt +++ b/src/main/kotlin/events/ParticleSpawnEvent.kt @@ -3,12 +3,12 @@ package moe.nea.firmament.events import org.joml.Vector3f -import net.minecraft.particle.ParticleEffect -import net.minecraft.util.math.Vec3d +import net.minecraft.core.particles.ParticleOptions +import net.minecraft.world.phys.Vec3 data class ParticleSpawnEvent( - val particleEffect: ParticleEffect, - val position: Vec3d, + val particleEffect: ParticleOptions, + val position: Vec3, val offset: Vector3f, val longDistance: Boolean, val count: Int, diff --git a/src/main/kotlin/events/PlayerInventoryUpdate.kt b/src/main/kotlin/events/PlayerInventoryUpdate.kt index 6e8203a..99bce9b 100644 --- a/src/main/kotlin/events/PlayerInventoryUpdate.kt +++ b/src/main/kotlin/events/PlayerInventoryUpdate.kt @@ -1,11 +1,22 @@ - package moe.nea.firmament.events -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack sealed class PlayerInventoryUpdate : FirmamentEvent() { - companion object : FirmamentEventBus<PlayerInventoryUpdate>() - data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() - data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() + companion object : FirmamentEventBus<PlayerInventoryUpdate>() + 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<ItemStack>) : 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 index 76c0b27..a276951 100644 --- a/src/main/kotlin/events/ProcessChatEvent.kt +++ b/src/main/kotlin/events/ProcessChatEvent.kt @@ -2,14 +2,14 @@ package moe.nea.firmament.events -import net.minecraft.text.Text +import net.minecraft.network.chat.Component 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() { +data class ProcessChatEvent(val text: Component, val wasExternallyCancelled: Boolean) : FirmamentEvent.Cancellable() { val unformattedString = text.unformattedString val nameHeuristic: String? = run { diff --git a/src/main/kotlin/events/ScreenChangeEvent.kt b/src/main/kotlin/events/ScreenChangeEvent.kt index 489e487..8193186 100644 --- a/src/main/kotlin/events/ScreenChangeEvent.kt +++ b/src/main/kotlin/events/ScreenChangeEvent.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.events -import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screens.Screen data class ScreenChangeEvent(val old: Screen?, val new: Screen?) : FirmamentEvent.Cancellable() { var overrideScreen: Screen? = null diff --git a/src/main/kotlin/events/ScreenRenderPostEvent.kt b/src/main/kotlin/events/ScreenRenderPostEvent.kt index 79f4913..43b7c01 100644 --- a/src/main/kotlin/events/ScreenRenderPostEvent.kt +++ b/src/main/kotlin/events/ScreenRenderPostEvent.kt @@ -2,15 +2,15 @@ package moe.nea.firmament.events -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen data class ScreenRenderPostEvent( val screen: Screen, val mouseX: Int, val mouseY: Int, val tickDelta: Float, - val drawContext: DrawContext + val drawContext: GuiGraphics ) : FirmamentEvent() { companion object : FirmamentEventBus<ScreenRenderPostEvent>() } diff --git a/src/main/kotlin/events/ServerConnectedEvent.kt b/src/main/kotlin/events/ServerConnectedEvent.kt index 26897f2..c9bc5e4 100644 --- a/src/main/kotlin/events/ServerConnectedEvent.kt +++ b/src/main/kotlin/events/ServerConnectedEvent.kt @@ -1,16 +1,16 @@ 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 +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientPacketListener +import net.minecraft.network.Connection data class ServerConnectedEvent( - val connection: ClientConnection + val connection: Connection ) : FirmamentEvent() { companion object : FirmamentEventBus<ServerConnectedEvent>() { init { - ClientPlayConnectionEvents.INIT.register(ClientPlayConnectionEvents.Init { clientPlayNetworkHandler: ClientPlayNetworkHandler, minecraftClient: MinecraftClient -> + ClientPlayConnectionEvents.INIT.register(ClientPlayConnectionEvents.Init { clientPlayNetworkHandler: ClientPacketListener, minecraftClient: Minecraft -> publishSync(ServerConnectedEvent(clientPlayNetworkHandler.connection)) }) } diff --git a/src/main/kotlin/events/SlotClickEvent.kt b/src/main/kotlin/events/SlotClickEvent.kt index d4abfb0..bfeadf9 100644 --- a/src/main/kotlin/events/SlotClickEvent.kt +++ b/src/main/kotlin/events/SlotClickEvent.kt @@ -1,15 +1,15 @@ 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.world.item.ItemStack +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType data class SlotClickEvent( val slot: Slot, val stack: ItemStack, val button: Int, - val actionType: SlotActionType, + val actionType: ClickType, ) : FirmamentEvent() { companion object : FirmamentEventBus<SlotClickEvent>() } diff --git a/src/main/kotlin/events/SlotRenderEvents.kt b/src/main/kotlin/events/SlotRenderEvents.kt index 5234176..c938ffc 100644 --- a/src/main/kotlin/events/SlotRenderEvents.kt +++ b/src/main/kotlin/events/SlotRenderEvents.kt @@ -2,19 +2,16 @@ 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 net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.inventory.Slot +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.render.drawGuiTexture interface SlotRenderEvents { - val context: DrawContext + val context: GuiGraphics val slot: Slot - fun highlight(sprite: Identifier) { + fun highlight(sprite: ResourceLocation) { context.drawGuiTexture( slot.x, slot.y, 0, 16, 16, sprite @@ -22,14 +19,14 @@ interface SlotRenderEvents { } data class Before( - override val context: DrawContext, override val slot: Slot, + override val context: GuiGraphics, override val slot: Slot, ) : FirmamentEvent(), SlotRenderEvents { companion object : FirmamentEventBus<Before>() } data class After( - override val context: DrawContext, override val slot: Slot, + override val context: GuiGraphics, override val slot: Slot, ) : FirmamentEvent(), SlotRenderEvents { companion object : FirmamentEventBus<After>() diff --git a/src/main/kotlin/events/SoundReceiveEvent.kt b/src/main/kotlin/events/SoundReceiveEvent.kt index d1b85b6..063f657 100644 --- a/src/main/kotlin/events/SoundReceiveEvent.kt +++ b/src/main/kotlin/events/SoundReceiveEvent.kt @@ -1,15 +1,15 @@ 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 +import net.minecraft.core.Holder +import net.minecraft.sounds.SoundSource +import net.minecraft.sounds.SoundEvent +import net.minecraft.world.phys.Vec3 data class SoundReceiveEvent( - val sound: RegistryEntry<SoundEvent>, - val category: SoundCategory, - val position: Vec3d, + val sound: Holder<SoundEvent>, + val category: SoundSource, + val position: Vec3, val pitch: Float, val volume: Float, val seed: Long diff --git a/src/main/kotlin/events/TickEvent.kt b/src/main/kotlin/events/TickEvent.kt index 18007f8..bf51774 100644 --- a/src/main/kotlin/events/TickEvent.kt +++ b/src/main/kotlin/events/TickEvent.kt @@ -3,5 +3,8 @@ package moe.nea.firmament.events data class TickEvent(val tickCount: Int) : FirmamentEvent() { + // TODO: introduce a client / server tick system. + // client ticks should ignore the game state + // server ticks should per-tick count packets received by the server companion object : FirmamentEventBus<TickEvent>() } diff --git a/src/main/kotlin/events/UseBlockEvent.kt b/src/main/kotlin/events/UseBlockEvent.kt index 8bbe0de..f229931 100644 --- a/src/main/kotlin/events/UseBlockEvent.kt +++ b/src/main/kotlin/events/UseBlockEvent.kt @@ -1,11 +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 +import net.minecraft.world.entity.player.Player +import net.minecraft.world.InteractionHand +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.level.Level -data class UseBlockEvent(val player: PlayerEntity, val world: World, val hand: Hand, val hitResult: BlockHitResult) : FirmamentEvent.Cancellable() { +data class UseBlockEvent(val player: Player, val world: Level, val hand: InteractionHand, val hitResult: BlockHitResult) : FirmamentEvent.Cancellable() { companion object : FirmamentEventBus<UseBlockEvent>() } diff --git a/src/main/kotlin/events/UseItemEvent.kt b/src/main/kotlin/events/UseItemEvent.kt index e294bb1..5174a51 100644 --- a/src/main/kotlin/events/UseItemEvent.kt +++ b/src/main/kotlin/events/UseItemEvent.kt @@ -1,11 +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 +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import net.minecraft.world.InteractionHand +import net.minecraft.world.level.Level -data class UseItemEvent(val playerEntity: PlayerEntity, val world: World, val hand: Hand) : FirmamentEvent.Cancellable() { +data class UseItemEvent(val playerEntity: Player, val world: Level, val hand: InteractionHand) : FirmamentEvent.Cancellable() { companion object : FirmamentEventBus<UseItemEvent>() - val item: ItemStack = playerEntity.getStackInHand(hand) + val item: ItemStack = playerEntity.getItemInHand(hand) } diff --git a/src/main/kotlin/events/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt index e8566fd..860db5c 100644 --- a/src/main/kotlin/events/WorldKeyboardEvent.kt +++ b/src/main/kotlin/events/WorldKeyboardEvent.kt @@ -1,18 +1,13 @@ - - 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<WorldKeyboardEvent>() +import moe.nea.firmament.keybindings.GenericInputAction +import moe.nea.firmament.keybindings.InputModifiers +import moe.nea.firmament.keybindings.SavedKeyBinding - fun matches(keyBinding: KeyBinding): Boolean { - return matches(IKeyBinding.minecraft(keyBinding)) - } +data class WorldKeyboardEvent(val action: GenericInputAction, val modifiers: InputModifiers) : FirmamentEvent.Cancellable() { + fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean { + return keyBinding.matches(action, modifiers, atLeast) + } - fun matches(keyBinding: IKeyBinding): Boolean { - return keyBinding.matches(keyCode, scanCode, modifiers) - } + companion object : FirmamentEventBus<WorldKeyboardEvent>() } 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<WorldMouseMoveEvent>() +} diff --git a/src/main/kotlin/events/WorldRenderLastEvent.kt b/src/main/kotlin/events/WorldRenderLastEvent.kt index 3c2103d..394a674 100644 --- a/src/main/kotlin/events/WorldRenderLastEvent.kt +++ b/src/main/kotlin/events/WorldRenderLastEvent.kt @@ -2,23 +2,20 @@ 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 +import net.minecraft.client.Camera +import net.minecraft.client.DeltaTracker +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.client.renderer.state.CameraRenderState +import com.mojang.blaze3d.vertex.PoseStack /** * 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, + val matrices: PoseStack, + val tickCounter: Int, + val camera: CameraRenderState, + val vertexConsumers: MultiBufferSource.BufferSource, ) : FirmamentEvent() { companion object : FirmamentEventBus<WorldRenderLastEvent>() } diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt index 1dcc91a..93a0f1a 100644 --- a/src/main/kotlin/events/registration/ChatEvents.kt +++ b/src/main/kotlin/events/registration/ChatEvents.kt @@ -1,19 +1,21 @@ package moe.nea.firmament.events.registration import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents 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 net.minecraft.network.chat.Component +import net.minecraft.world.InteractionResult import moe.nea.firmament.events.AllowChatEvent import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.JoinServerEvent 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 +private var lastReceivedMessage: Component? = null fun registerFirmamentEvents() { ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp -> @@ -43,21 +45,24 @@ fun registerFirmamentEvents() { AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction -> if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled) - ActionResult.CONSUME - else ActionResult.PASS + InteractionResult.FAIL + else InteractionResult.PASS }) UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled) - ActionResult.CONSUME - else ActionResult.PASS + InteractionResult.FAIL + else InteractionResult.PASS }) UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> if (UseItemEvent.publish(UseItemEvent(player, world, hand)).cancelled) - ActionResult.CONSUME - else ActionResult.PASS + InteractionResult.FAIL + else InteractionResult.PASS }) UseItemCallback.EVENT.register(UseItemCallback { playerEntity, world, hand -> - if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.CONSUME - else ActionResult.PASS + if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) InteractionResult.FAIL + else InteractionResult.PASS }) + ClientPlayConnectionEvents.JOIN.register { networkHandler, packetSender, _ -> + JoinServerEvent.publish(JoinServerEvent(networkHandler, packetSender)) + } } diff --git a/src/main/kotlin/events/subscription/Subscription.kt b/src/main/kotlin/events/subscription/Subscription.kt index 1c1d3bd..812da92 100644 --- a/src/main/kotlin/events/subscription/Subscription.kt +++ b/src/main/kotlin/events/subscription/Subscription.kt @@ -3,11 +3,7 @@ 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<T : FirmamentEvent>( val owner: Any, diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 0f5ebf8..d474d65 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -1,123 +1,25 @@ 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.buttons.InventoryButtons -import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay -import moe.nea.firmament.features.mining.PickaxeAbility -import moe.nea.firmament.features.mining.PristineProfitTracker -import moe.nea.firmament.features.world.FairySouls -import moe.nea.firmament.features.world.Waypoints -import moe.nea.firmament.util.data.DataHolder - -object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { - @Serializable - data class Config( - val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() - ) - - private val features = mutableMapOf<String, FirmamentFeature>() - - val allFeatures: Collection<FirmamentFeature> get() = features.values - - private var hasAutoloaded = false - - init { - autoload() - } - - 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(InventoryButtons) - loadFeature(CompatibliltyFeatures) - loadFeature(AnniversaryFeatures) - loadFeature(QuickCommands) - loadFeature(PetFeatures) - loadFeature(SaveCursorPosition) - loadFeature(PriceData) - loadFeature(Fixes) - 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 { - it.provideSubscriptions { - it.owner.javaClass.classes.forEach { - runCatching { it.getDeclaredField("INSTANCE").get(null) } +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.compatloader.ICompatMeta + +object FeatureManager { + + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + ErrorUtil.catch("Error while loading events from $list") { + list.provideSubscriptions { + subscribeSingleEvent(it) + } } - subscribeSingleEvent(it) - } - } - } - - private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { - 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() - } + } + } + private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } } diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt deleted file mode 100644 index 2cfc4fd..0000000 --- a/src/main/kotlin/features/FirmamentFeature.kt +++ /dev/null @@ -1,23 +0,0 @@ - - -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 index 9e0de40..f13fe7e 100644 --- a/src/main/kotlin/features/chat/AutoCompletions.kt +++ b/src/main/kotlin/features/chat/AutoCompletions.kt @@ -1,6 +1,15 @@ package moe.nea.firmament.features.chat +import com.mojang.brigadier.Message import com.mojang.brigadier.arguments.StringArgumentType.string +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.BuiltInExceptions +import com.mojang.brigadier.exceptions.CommandExceptionType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import kotlin.concurrent.thread +import net.minecraft.SharedConstants +import net.minecraft.commands.BrigadierExceptions import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.get import moe.nea.firmament.commands.suggestsList @@ -8,21 +17,21 @@ 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 +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr -object AutoCompletions : FirmamentFeature { +object AutoCompletions { + @Config 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 + val identifier: String get() = "auto-completions" @Subscribe @@ -44,12 +53,20 @@ object AutoCompletions : FirmamentFeature { thenExecute { val warpName = get(toArg) if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) { - MC.sendServerCommand("warp island") + MC.sendCommand("warp island") } else { - MC.sendServerCommand("warp $warpName") + redirectToServer() } } } } } + + fun CommandContext<*>.redirectToServer() { + val message = tr( + "firmament.warp.auto-complete.internal-throw", + "This is an internal syntax exception that should not show up in gameplay, used to pass on a command to the server" + ) + throw CommandSyntaxException(CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand(), message) + } } diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt index f85825b..aca7af8 100644 --- a/src/main/kotlin/features/chat/ChatLinks.kt +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -1,47 +1,48 @@ 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.URL +import java.net.URI import java.util.Collections import java.util.concurrent.atomic.AtomicInteger -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.future.await 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 net.minecraft.client.gui.screens.ChatScreen +import com.mojang.blaze3d.platform.NativeImage +import net.minecraft.client.renderer.texture.DynamicTexture +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.HoverEvent +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.resources.ResourceLocation 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.jarvis.JarvisIntegration import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.net.HttpUtil 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 +object ChatLinks { + val identifier: String get() = "chat-links" + @Config 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) } + val position by position("position", 16 * 20, 9 * 20) { Vector2i(0, 0) } } private fun isHostAllowed(host: String) = @@ -49,12 +50,11 @@ object ChatLinks : FirmamentFeature { private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) - override val config get() = TConfig - val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex() + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex() val nextTexId = AtomicInteger(0) data class Image( - val texture: Identifier, + val texture: ResourceLocation, val width: Int, val height: Int, ) @@ -71,18 +71,16 @@ object ChatLinks : FirmamentFeature { } 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(image) - ) - Image(texId, image.width, image.height) - } else - null + val inputStream = HttpUtil.request(url) + .forInputStream() + .await() + val image = NativeImage.read(inputStream) + val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") + MC.textureManager.register( + texId, + DynamicTexture({ texId.path }, image) + ) + Image(texId, image.width, image.height) } catch (exc: Exception) { exc.printStackTrace() null @@ -101,19 +99,19 @@ object ChatLinks : FirmamentFeature { 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 ?: return - val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + MC.inGameHud.chat.getClickedComponentStyleAt(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() + it.drawContext.pose().pushMatrix() val pos = TConfig.position - pos.applyTransformations(it.drawContext.matrices) + pos.applyTransformations(JarvisIntegration.jarvis, it.drawContext.pose()) val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) - it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.pose().scale(scale, scale) it.drawContext.drawTexture( image.texture, 0, @@ -125,7 +123,7 @@ object ChatLinks : FirmamentFeature { image.width, image.height, ) - it.drawContext.matrices.pop() + it.drawContext.pose().popMatrix() } @Subscribe @@ -134,23 +132,24 @@ object ChatLinks : FirmamentFeature { it.replaceWith = it.replaceWith.transformEachRecursively { child -> val text = child.string if ("://" !in text) return@transformEachRecursively child - val s = Text.empty().setStyle(child.style) + val s = Component.empty().setStyle(child.style) var index = 0 while (index < text.length) { val nextMatch = urlRegex.find(text, index) - if (nextMatch == null) { - s.append(Text.literal(text.substring(index, text.length))) + val url = nextMatch?.groupValues[0] + val uri = runCatching { url?.let(::URI) }.getOrNull() + if (nextMatch == null || url == null || uri == null) { + s.append(Component.literal(text.substring(index, text.length))) break } val range = nextMatch.groups[0]!!.range - val url = nextMatch.groupValues[0] - s.append(Text.literal(text.substring(index, range.first))) + s.append(Component.literal(text.substring(index, range.first))) s.append( - Text.literal(url).setStyle( - Style.EMPTY.withUnderline(true).withColor( - Formatting.AQUA - ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) - .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) + Component.literal(url).setStyle( + Style.EMPTY.withUnderlined(true).withColor( + ChatFormatting.AQUA + ).withHoverEvent(HoverEvent.ShowText(Component.literal(url))) + .withClickEvent(ClickEvent.OpenUrl(uri)) ) ) if (isImageUrl(url)) diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt new file mode 100644 index 0000000..6bef99f --- /dev/null +++ b/src/main/kotlin/features/chat/CopyChat.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.features.chat + +import net.minecraft.util.FormattedCharSequence +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.reconstitute + + +object CopyChat { + val identifier: String + get() = "copy-chat" + + @Config + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val copyChat by toggle("copy-chat") { false } + } + + fun orderedTextToString(orderedText: FormattedCharSequence): String { + return orderedText.reconstitute().string + } +} diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt index de3a0d9..85daf51 100644 --- a/src/main/kotlin/features/chat/PartyCommands.kt +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -5,17 +5,18 @@ 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 net.minecraft.core.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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.tr import moe.nea.firmament.util.useMatch @@ -79,7 +80,7 @@ object PartyCommands { register("coords") { executes { - val p = MC.player?.blockPos ?: BlockPos.ORIGIN + val p = MC.player?.blockPosition() ?: BlockPos.ZERO MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}") 0 } @@ -89,6 +90,7 @@ object PartyCommands { // TODO: at TPS command } + @Config object TConfig : ManagedConfig("party-commands", Category.CHAT) { val enable by toggle("enable") { false } val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds } diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt index 7963171..b857f8a 100644 --- a/src/main/kotlin/features/chat/QuickCommands.kt +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -5,9 +5,9 @@ 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 net.minecraft.commands.CommandBuildContext +import net.minecraft.network.protocol.game.ClientboundCommandsPacket +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.DefaultSource import moe.nea.firmament.commands.RestArgumentType @@ -15,18 +15,19 @@ 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.grey import moe.nea.firmament.util.tr -object QuickCommands : FirmamentFeature { - override val identifier: String +object QuickCommands { + val identifier: String get() = "quick-commands" + @Config object TConfig : ManagedConfig("quick-commands", Category.CHAT) { val enableJoin by toggle("join") { true } val enableDh by toggle("dh") { true } @@ -43,10 +44,14 @@ object QuickCommands : FirmamentFeature { val dispatcher = CommandDispatcher<FabricClientCommandSource>() ClientCommandInternals.setActiveDispatcher(dispatcher) ClientCommandRegistrationCallback.EVENT.invoker() - .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries, - network.enabledFeatures)) + .register( + dispatcher, CommandBuildContext.simple( + network.registryAccess, + network.enabledFeatures() + ) + ) ClientCommandInternals.finalizeInit() - network.onCommandTree(lastPacket) + network.handleCommands(lastPacket) } catch (ex: Exception) { ClientCommandInternals.setActiveDispatcher(fallback) throw ex @@ -64,7 +69,7 @@ object QuickCommands : FirmamentFeature { return lf } - var lastReceivedTreePacket: CommandTreeS2CPacket? = null + var lastReceivedTreePacket: ClientboundCommandsPacket? = null val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") @@ -98,16 +103,20 @@ object QuickCommands : FirmamentFeature { } val joinName = getNameForFloor(what.replace(" ", "").lowercase()) if (joinName == null) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) + source.sendFeedback(Component.translatableEscape("firmament.quick-commands.join.unknown", what)) } else { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", - joinName)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.success", + joinName + ) + ) MC.sendCommand("joininstance $joinName") } } } thenExecute { - source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) + source.sendFeedback(Component.translatable("firmament.quick-commands.join.explain")) } } } @@ -122,8 +131,12 @@ object QuickCommands : FirmamentFeature { ) } if (l !in kuudraLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", - kuudraLevel)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.unknown-kuudra", + kuudraLevel + ) + ) return null } return "KUUDRA_${kuudraLevelNames[l]}" @@ -143,8 +156,12 @@ object QuickCommands : FirmamentFeature { return "CATACOMBS_ENTRANCE" } if (l !in dungeonLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", - kuudraLevel)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.unknown-catacombs", + kuudraLevel + ) + ) return null } return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..dc77115 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.commands.arguments.ResourceKeyArgument +import net.minecraft.core.component.DataComponentType +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.Tag +import net.minecraft.nbt.NbtOps +import net.minecraft.core.registries.Registries +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<T>( + val prism: NbtPrism, + val component: DataComponentType<T>, + ) { + fun observe(itemStack: ItemStack): Collection<Tag> { + 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<String> = mutableListOf() + val metaHistory: MutableList<List<String>> = 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<String>, List<String>) -> List<String>): List<String> { + 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.level.getEntitiesOfClass( + ArmorStand::class.java, + p.boundingBox.inflate(10.0), + { it.isMarker }) + .minBy { it.distanceToSqr(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.crosshairPickEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + ResourceKeyArgument.key(Registries.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).location()}" + ) + ) + } + } + } + } + } + } + } + + private fun copyHistory(toCopy: List<String>) { + 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 index 2c6b962..2c8ced0 100644 --- a/src/main/kotlin/features/debug/DebugLogger.kt +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -1,24 +1,29 @@ package moe.nea.firmament.features.debug import kotlinx.serialization.serializer -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TestUtil import moe.nea.firmament.util.collections.InstanceList +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder class DebugLogger(val tag: String) { companion object { val allInstances = InstanceList<DebugLogger>("DebugLogger") } + + @Config object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf) init { allInstances.add(this) } - fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun isEnabled() = TestUtil.isInTest || EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } fun log(text: () -> String) { if (!isEnabled()) return - MC.sendChat(Text.literal(text())) + MC.sendChat(Component.literal(text())) } } diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt deleted file mode 100644 index ee54260..0000000 --- a/src/main/kotlin/features/debug/DebugView.kt +++ /dev/null @@ -1,29 +0,0 @@ - - -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<T>( - val obj: T, - val timer: TimeMark, - ) - - private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf() - override val identifier: String - get() = "debug-view" - override val defaultEnabled: Boolean - get() = Firmament.DEBUG - - fun <T : Any?> 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 index 8f0c25c..8638bb6 100644 --- a/src/main/kotlin/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -3,33 +3,38 @@ 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 net.minecraft.client.Minecraft +import net.minecraft.network.chat.Component 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.iterate -object DeveloperFeatures : FirmamentFeature { - override val identifier: String +object DeveloperFeatures { + val DEVELOPER_SUBCOMMAND: String = "dev" + 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() } + @Config object TConfig : ManagedConfig("developer", Category.DEV) { val autoRebuildResources by toggle("auto-rebuild") { false } } @@ -42,6 +47,42 @@ object DeveloperFeatures : FirmamentFeature { } @Subscribe + fun loadAllMixinClasses(event: DebugInstantiateEvent) { + val allMixinClasses = mutableSetOf<String>() + 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<Mixin>() + (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 @@ -51,24 +92,27 @@ object DeveloperFeatures : FirmamentFeature { } @JvmStatic - fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> { - val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) { + fun hookOnBeforeResourceReload(client: Minecraft): CompletableFuture<Void> { + val reloadFuture = if (TConfig.autoRebuildResources && Firmament.DEBUG && 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")) + MC.sendChat(Component.translatable("firmament.dev.resourcerebuild.start")) val startTime = TimeMark.now() process.toHandle().onExit().thenApply { - MC.sendChat(Text.stringifiedTranslatable( - "firmament.dev.resourcerebuild.done", - startTime.passedTime())) + MC.sendChat( + Component.translatableEscape( + "firmament.dev.resourcerebuild.done", + startTime.passedTime() + ) + ) Unit } } else { CompletableFuture.completedFuture(Unit) } - return reloadFuture.thenCompose { client.reloadResources() } + return reloadFuture.thenCompose { client.reloadResourcePacks() } } } diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..a2b42fd --- /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<String>, +) { + companion object { + val current = ExportedTestConstantMeta( + SharedConstants.getCurrentVersion().dataVersion().version, + Optional.of("Firmament ${Firmament.version.friendlyString}") + ) + + val CODEC: Codec<ExportedTestConstantMeta> = 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 index 32035a6..7936521 100644 --- a/src/main/kotlin/features/debug/MinorTrolling.kt +++ b/src/main/kotlin/features/debug/MinorTrolling.kt @@ -2,15 +2,13 @@ package moe.nea.firmament.features.debug -import net.minecraft.text.Text +import net.minecraft.network.chat.Component 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 +object MinorTrolling { + val identifier: String get() = "minor-trolling" val trollers = listOf("nea89o", "lrg89") @@ -22,6 +20,6 @@ object MinorTrolling : FirmamentFeature { val (_, name, text) = m.groupValues if (name !in trollers) return if (!text.startsWith("c:")) return - it.replaceWith = Text.literal(text.substring(2).replace("&", "§")) + it.replaceWith = Component.literal(text.substring(2).replace("&", "§")) } } diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 225bc13..145ea35 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -2,43 +2,55 @@ 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.world.level.block.SkullBlock +import net.minecraft.world.level.block.entity.SkullBlockEntity +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -import net.minecraft.text.Text -import net.minecraft.text.TextCodecs -import net.minecraft.util.Identifier -import net.minecraft.util.hit.BlockHitResult -import net.minecraft.util.hit.EntityHitResult -import net.minecraft.util.hit.HitResult +import net.minecraft.advancements.critereon.NbtPredicate +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.Nameable +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.EntityHitResult +import net.minecraft.world.phys.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.SlotRenderEvents 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.grey +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.mc.unsafeNbt import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr -object PowerUserTools : FirmamentFeature { - override val identifier: String +object PowerUserTools { + val identifier: String get() = "power-user" + @Config object TConfig : ManagedConfig(identifier, Category.DEV) { val showItemIds by toggle("show-item-id") { false } val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") @@ -48,22 +60,26 @@ object PowerUserTools : FirmamentFeature { 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 } + val showSlotNumbers by keyBindingWithDefaultUnbound("slot-numbers") + val autoCopyAnimatedSkins by toggle("copy-animated-skins") { false } } - override val config - get() = TConfig - - var lastCopiedStack: Pair<ItemStack, Text>? = null + var lastCopiedStack: Pair<ItemStack, Component>? = null set(value) { field = value - if (value != null) lastCopiedStackViewTime = true + if (value != null) lastCopiedStackViewTime = 2 } - var lastCopiedStackViewTime = false + var lastCopiedStackViewTime = 0 @Subscribe fun resetLastCopiedStack(event: TickEvent) { - if (!lastCopiedStackViewTime) lastCopiedStack = null - lastCopiedStackViewTime = false + if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null } @Subscribe @@ -71,39 +87,58 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = null } - fun debugFormat(itemStack: ItemStack): Text { - return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + fun debugFormat(itemStack: ItemStack): Component { + return Component.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + } + + @Subscribe + fun onRender(event: SlotRenderEvents.After) { + if (TConfig.showSlotNumbers.isPressed()) { + event.context.drawString( + MC.font, + event.slot.index.toString(), event.slot.x, event.slot.y, 0xFF00FF00.toInt(), true + ) + event.context.drawString( + MC.font, + event.slot.containerSlot.toString(), event.slot.x, event.slot.y + MC.font.lineHeight, 0xFFFF0000.toInt(), true + ) + } } @Subscribe fun onEntityInfo(event: WorldKeyboardEvent) { if (!event.matches(TConfig.copyEntityData)) return - val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity + val target = (MC.instance.hitResult as? EntityHitResult)?.entity if (target == null) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.fail")) + MC.sendChat(Component.translatable("firmament.poweruser.entity.fail")) return } showEntity(target) } fun showEntity(target: Entity) { - 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)) + val nbt = NbtPredicate.getEntityTagToCompare(target) + nbt.remove("Inventory") + nbt.put("StyledName", ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, target.feedbackDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.type", target.type)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.name", target.name)) + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.position", target.position)) if (target is LivingEntity) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) - for (armorItem in target.armorItems) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor")) + for ((slot, armorItem) in target.iterableArmorItems) { + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) } } - MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size)) - target.passengerList.forEach { + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.passengers", target.passengers.size)) + target.passengers.forEach { showEntity(it) } } // TODO: leak this through some other way, maybe. - lateinit var getSkullId: (profile: ProfileComponent) -> Identifier? + lateinit var getSkullId: (profile: ResolvableProfile) -> ResourceLocation? @Subscribe fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { @@ -112,58 +147,74 @@ object PowerUserTools : FirmamentFeature { if (it.matches(TConfig.copyItemId)) { val sbId = item.skyBlockId if (sbId == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skyblockid.fail")) return } ClipboardUtils.setTextContent(sbId.neuItem) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.skyblockid", sbId.neuItem)) } else if (it.matches(TConfig.copyTexturePackId)) { - val model = CustomItemModelEvent.getModelIdentifier(item) // TODO: remove global texture overrides, maybe + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: ResourceLocation): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.modelid.fail")) return } ClipboardUtils.setTextContent(model.toString()) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) + Pair(item, Component.translatableEscape("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() ?: "<empty>" + val nbt = item.get(DataComponents.CUSTOM_DATA)?.unsafeNbt?.toPrettyString() ?: "<empty>" ClipboardUtils.setTextContent(nbt) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) + lastCopiedStack = Pair(item, Component.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() + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() }) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.lore")) + lastCopiedStack = Pair(item, Component.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")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) return } - val profile = item.get(DataComponentTypes.PROFILE) + val profile = item.get(DataComponents.PROFILE) if (profile == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) + lastCopiedStack = Pair(item, Component.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")) + lastCopiedStack = Pair(item, Component.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())) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.skull-id", skullTexture.toString())) println("Copied skull id: $skullTexture") } else if (it.matches(TConfig.copyItemStack)) { - ClipboardUtils.setTextContent( - ItemStack.CODEC - .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) - .orThrow.toPrettyString()) - lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.createSerializationContext(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) + lastCopiedStack = Pair(item, Component.translatableEscape("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle) && it.screen is AbstractContainerScreen<*>) { + val allTitles = ListTag() + val inventoryNames = + it.screen.menu.slots + .mapNotNullTo(mutableSetOf()) { it.container } + .filterIsInstance<Nameable>() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) } } @@ -171,23 +222,23 @@ object PowerUserTools : FirmamentFeature { fun onCopyWorldInfo(it: WorldKeyboardEvent) { if (it.matches(TConfig.copySkullTexture)) { val p = MC.camera ?: return - val blockHit = p.raycast(20.0, 0.0f, false) ?: return + val blockHit = p.pick(20.0, 0.0f, false) ?: return if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.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")) + val blockAt = p.level.getBlockState(blockHit.blockPos)?.block + val entity = p.level.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.ownerProfile == null) { + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) return } - val id = getSkullId(entity.owner!!) + val id = getSkullId(entity.ownerProfile!!) if (id == null) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) } else { ClipboardUtils.setTextContent(id.toString()) - MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString())) + MC.sendChat(Component.translatableEscape("firmament.tooltip.copied.skull", id.toString())) } } } @@ -196,14 +247,14 @@ object PowerUserTools : FirmamentFeature { fun addItemId(it: ItemTooltipEvent) { if (TConfig.showItemIds) { val id = it.stack.skyBlockId ?: return - it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem)) + it.lines.add(Component.translatableEscape("firmament.tooltip.skyblockid", id.neuItem).grey()) } val (item, text) = lastCopiedStack ?: return - if (!ItemStack.areEqual(item, it.stack)) { + if (!ItemStack.matches(item, it.stack)) { lastCopiedStack = null return } - lastCopiedStackViewTime = true + lastCopiedStackViewTime = 0 it.lines.add(text) } diff --git a/src/main/kotlin/features/debug/SkinPreviews.kt b/src/main/kotlin/features/debug/SkinPreviews.kt new file mode 100644 index 0000000..56c63db --- /dev/null +++ b/src/main/kotlin/features/debug/SkinPreviews.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.features.debug + +import com.mojang.authlib.GameProfile +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.time.Duration.Companion.seconds +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.phys.Vec3 +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.IsSlotProtectedEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.rawSkyBlockId +import moe.nea.firmament.util.toTicks +import moe.nea.firmament.util.tr + + +object SkinPreviews { + + // TODO: add pet support + @Subscribe + fun onEntityUpdate(event: EntityUpdateEvent) { + if (!isRecording) return + if (event.entity.position != pos) + return + val entity = event.entity as? LivingEntity ?: return + val stack = entity.getItemBySlot(EquipmentSlot.HEAD) ?: return + val profile = stack.get(DataComponents.PROFILE)?.partialProfile() ?: return + if (profile == animation.lastOrNull()) return + animation.add(profile) + val shortened = animation.shortenCycle() + if (shortened.size <= (animation.size / 2).coerceAtLeast(1) && lastDiscard.passedTime() > 2.seconds) { + val tickEstimation = (lastDiscard.passedTime() / animation.size).toTicks() + val skinName = if (skinColor != null) "${skinId}_${skinColor?.replace(" ", "_")?.uppercase()}" else skinId!! + val json = + buildJsonObject { + put("ticks", tickEstimation) + put( + "textures", + shortened.map { + it.id.toString() + ":" + it.properties()["textures"].first().value() + }.toJsonArray() + ) + } + MC.sendChat( + tr( + "firmament.dev.skinpreviews.done", + "Observed a total of ${animation.size} elements, which could be shortened to a cycle of ${shortened.size}. Copying JSON array. Estimated ticks per frame: $tickEstimation." + ) + ) + isRecording = false + ClipboardUtils.setTextContent(JsonPrimitive(skinName).toString() + ":" + json.toString()) + } + } + + var animation = mutableListOf<GameProfile>() + var pos = Vec3(-1.0, 72.0, -101.25) + var isRecording = false + var skinColor: String? = null + var skinId: String? = null + var lastDiscard = TimeMark.farPast() + + @Subscribe + fun onActivate(event: IsSlotProtectedEvent) { + if (!PowerUserTools.TConfig.autoCopyAnimatedSkins) return + val lastLine = event.itemStack.loreAccordingToNbt.lastOrNull()?.string + if (lastLine != "Right-click to preview!" && lastLine != "Click to preview!") return + lastDiscard = TimeMark.now() + val stackName = event.itemStack.displayNameAccordingToNbt.string + if (stackName == "FIRE SALE!") { + skinColor = null + skinId = event.itemStack.rawSkyBlockId + } else { + skinColor = stackName + } + animation.clear() + isRecording = true + MC.sendChat(tr("firmament.dev.skinpreviews.start", "Starting to observe items")) + } +} diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt new file mode 100644 index 0000000..37b248a --- /dev/null +++ b/src/main/kotlin/features/debug/SoundVisualizer.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.network.chat.Component +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<SoundReceiveEvent>() + + + @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( + Component.literal(event.sound.value().location.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..d12d667 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt @@ -0,0 +1,256 @@ +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.player.AbstractClientPlayer +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.core.ClientAsset +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.crosshairPickEntity + if (entity == null) { + MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export")) + return + } + Firmament.coroutineScope.launch { + val guessName = entity.level.getEntitiesOfClass( + ArmorStand::class.java, + entity.boundingBox.inflate(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? AbstractClientPlayer + val textureUrl = (playerEntity?.skin?.body as? ClientAsset.DownloadedTexture)?.url + 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)?.item + val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false) + if (craftingTableSlot?.item?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") { + slotIndices.forEach { (_, index) -> + event.screen.getSlotByIndex(index, false)?.item?.let(ItemExporter::ensureExported) + } + val inputs = slotIndices.associate { (name, index) -> + val id = event.screen.getSlotByIndex(index, false)?.item?.takeIf { !it.isEmpty() }?.let { + "${it.skyBlockId?.neuItem}:${it.count}" + } ?: "" + name to JsonPrimitive(id) + } + val output = event.screen.getSlotByIndex(resultSlot, false)?.item!! + 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)?.item ?: 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 = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern() + private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern() + private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern() + + private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".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<SkyblockId, Double>? { + 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..f664da3 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt @@ -0,0 +1,250 @@ +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.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.StringTag +import net.minecraft.network.chat.Component +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): Component { + nonOverlayCache.clear() + val exporter = LegacyItemExporter.createExporter(itemStack) + var json = exporter.exportJson() + val fileName = json.jsonObject["internalname"]?.jsonPrimitive?.takeIf { it.isString }?.content + if (fileName == null) { + return tr( + "firmament.repoexport.nointernalname", + "Could not find internal name to export for this item (null.json)" + ) + } + val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json") + itemFile.createParentDirectories() + if (itemFile.exists()) { + val existing = try { + Firmament.json.decodeFromString<JsonObject>(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<JsonObject>(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 { StringTag.valueOf(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<SkyblockId, Boolean>() + + @Subscribe + fun onRender(event: SlotRenderEvents.Before) { + if (!PowerUserTools.TConfig.highlightNonOverlayItems) { + return + } + val stack = event.slot.item ?: return + val id = event.slot.item.skyBlockId?.neuItem + if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return + val sbId = stack.skyBlockId ?: return + val isExported = nonOverlayCache.getOrPut(sbId) { + RepoManager.overlayData.getOverlayFiles(sbId).isNotEmpty() || // This extra case is here so that an export works immediately, without repo reload + 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 = Component.literal(title) + it.loreAccordingToNbt = listOf(Component.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..2bc51bc --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt @@ -0,0 +1,87 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.Serializable +import net.minecraft.nbt.CompoundTag +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.util.StringUtil.camelWords +import moe.nea.firmament.util.mc.loadItemFromNbt + +/** + * 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<Variation> = 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 <reified T : Any> getLegacyData(name: String) = + Firmament.tryDecodeJsonFromStream<T>( + LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!! + ).getOrThrow() + + val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments") + val enchantmentLut = enchantmentData.associateBy { ResourceLocation.withDefaultNamespace(it.name) } + + val itemDat = getLegacyData<List<ItemData>>("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(CompoundTag().apply { + putString("id", legacyItemType.name) + putByte("Count", 1) + putShort("Damage", legacyItemType.metadata) + })!! + nbt.remove("components") + val stack = loadItemFromNbt(nbt) ?: error("Could not transform $legacyItemType: $nbt") + stack.item to legacyItemType + } + }.toMap() + + @Serializable + data class LegacyEffect( + val id: Int, + val name: String, + val displayName: String, + val type: String + ) + + val effectList = getLegacyData<List<LegacyEffect>>("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..7b855d1 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt @@ -0,0 +1,318 @@ +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.core.component.DataComponents +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.StringTag +import net.minecraft.tags.ItemTags +import net.minecraft.network.chat.Component +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.MC +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.modifyExtraAttributes +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 + val originalId = itemStack.extraAttributes.getString("id") + var name = itemStack.displayNameAccordingToNbt + val extraAttribs = itemStack.extraAttributes.copy() + val legacyNbt = CompoundTag() + val warnings = mutableListOf<String>() + + // 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<HypixelPetInfo>(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.defaultInstance.loreAccordingToNbt + itemStack.remove(DataComponents.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}") + Component.literal(string).setStyle(it.style) + } + + if (lore.isEmpty()) + lore = listOf(Component.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] = + Component.literal(last.directLiteralStringContent!!.trimEnd()) + .setStyle(last.style) + lore[index] = v + } + this.lore = lore + } + + fun collapseWhitespaces() { + lore = (listOf(null as Component?) + 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", IntTag.valueOf(254)) + copyUnbreakable() + copyItemModel() + copyPotion() + copyExtraAttributes() + copyLegacySkullNbt() + copyDisplay() + copyColour() + copyEnchantments() + copyEnchantGlint() + // TODO: copyDisplay + } + + private fun copyPotion() { + val effects = itemStack.get(DataComponents.POTION_CONTENTS) ?: return + legacyNbt.put("CustomPotionEffects", ListTag().also { + effects.allEffects.forEach { effect -> + val effectId = effect.effect.unwrapKey().get().location().path + val duration = effect.duration + val legacyId = LegacyItemData.effectList[effectId]!! + + it.add(CompoundTag().apply { + put("Ambient", ByteTag.valueOf(false)) + put("Duration", IntTag.valueOf(duration)) + put("Id", ByteTag.valueOf(legacyId.id.toByte())) + put("Amplifier", ByteTag.valueOf(effect.amplifier.toByte())) + }) + } + }) + } + + fun CompoundTag.getOrPutCompound(name: String): CompoundTag { + val compound = getCompoundOrEmpty(name) + put(name, compound) + return compound + } + + private fun copyColour() { + if (!itemStack.`is`(ItemTags.DYEABLE)) { + itemStack.remove(DataComponents.DYED_COLOR) + return + } + val leatherTint = itemStack.componentsPatch.get(DataComponents.DYED_COLOR)?.getOrNull() ?: return + legacyNbt.getOrPutCompound("display").put("color", IntTag.valueOf(leatherTint.rgb)) + } + + private fun copyItemModel() { + val itemModel = itemStack.get(DataComponents.ITEM_MODEL) ?: return + legacyNbt.put("ItemModel", StringTag.valueOf(itemModel.toString())) + } + + private fun copyDisplay() { + legacyNbt.getOrPutCompound("display").apply { + put("Lore", lore.map { StringTag.valueOf(it.getLegacyFormatString(trimmed = true)) }.toNbtList()) + putString("Name", name.getLegacyFormatString(trimmed = true)) + } + } + + fun exportModernSnbt(): Tag { + val overlay = ItemStack.CODEC.encodeStart(MC.currentOrDefaultRegistryNbtOps, itemStack.copy().also { + it.modifyExtraAttributes { attribs -> + originalId.ifPresent { attribs.putString("id", it) } + } + }).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(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) == true) { + val ench = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", ench) + } + } + + private fun copyUnbreakable() { + if (itemStack.get(DataComponents.UNBREAKABLE) == Unit.INSTANCE) { + legacyNbt.putBoolean("Unbreakable", true) + } + } + + fun copyEnchantments() { + val enchantments = itemStack.get(DataComponents.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return + val enchTag = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", enchTag) + enchantments.entrySet().forEach { entry -> + val id = entry.key.unwrapKey().get().location() + val legacyId = LegacyItemData.enchantmentLut[id] + if (legacyId == null) { + warnings.add("Could not find legacy enchantment id for ${id}") + return@forEach + } + enchTag.add(CompoundTag().apply { + putShort("lvl", entry.intValue.toShort()) + putShort( + "id", + legacyId.id.toShort() + ) + }) + } + } + + fun copyExtraAttributes() { + legacyNbt.put("ExtraAttributes", extraAttribs) + } + + fun copyLegacySkullNbt() { + val profile = itemStack.get(DataComponents.PROFILE) ?: return + legacyNbt.put("SkullOwner", CompoundTag().apply { + putString("Id", profile.partialProfile().id.toString()) + putBoolean("hypixelPopulated", true) + put("Properties", CompoundTag().apply { + profile.partialProfile().properties().forEach { prop, value -> + val list = getListOrEmpty(prop) + put(prop, list) + list.add(CompoundTag().apply { + value.signature?.let { + putString("Signature", it) + } + putString("Value", value.value) + putString("Name", value.name) + }) + } + }) + }) + } + + fun legacyifyItemStack(): LegacyItemData.LegacyItemType { + // TODO: add a default here + if (itemStack.item == Items.LINGERING_POTION || itemStack.item == Items.SPLASH_POTION) + return LegacyItemData.LegacyItemType("potion", 16384) + return LegacyItemData.itemLut[itemStack.item]!! + } +} diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt index ff85c00..c12a2c6 100644 --- a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt +++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt @@ -1,33 +1,30 @@ 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 net.minecraft.core.particles.ParticleTypes +import net.minecraft.sounds.SoundEvents +import net.minecraft.world.phys.Vec3 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 { +object AncestralSpadeSolver { var lastDing = TimeMark.farPast() private set private val pitches = mutableListOf<Float>() - val particlePositions = mutableListOf<Vec3d>() - var nextGuess: Vec3d? = null + val particlePositions = mutableListOf<Vec3>() + var nextGuess: Vec3? = null private set private var lastTeleportAttempt = TimeMark.farPast() @@ -35,7 +32,11 @@ object AncestralSpadeSolver : SubscriptionOwner { 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 + && MC.player?.inventory?.hasAnyMatching { + it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE || + it.skyBlockId == SkyBlockItems.ARCHAIC_SPADE || + it.skyBlockId == SkyBlockItems.DEIFIC_SPADE + } == true // TODO: add a reactive property here @Subscribe fun onKeyBind(event: WorldKeyboardEvent) { @@ -62,7 +63,7 @@ object AncestralSpadeSolver : SubscriptionOwner { @Subscribe fun onPlaySound(event: SoundReceiveEvent) { if (!isEnabled()) return - if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return + if (!SoundEvents.NOTE_BLOCK_HARP.`is`(event.sound.value().location)) return if (lastDing.passedTime() > 1.seconds) { particlePositions.clear() @@ -96,7 +97,7 @@ object AncestralSpadeSolver : SubscriptionOwner { .let { (a, _, b) -> b.subtract(a) } .normalize() - nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate)) + nextGuess = event.position.add(lastParticleDirection.scale(soundDistanceEstimate)) } @Subscribe @@ -106,13 +107,11 @@ object AncestralSpadeSolver : SubscriptionOwner { nextGuess?.let { tinyBlock(it, 1f, 0x80FFFFFF.toInt()) // TODO: replace this - color(1f, 1f, 0f, 1f) - tracer(it, lineWidth = 3f) + tracer(it, lineWidth = 3f, color = 0x80FFFFFF.toInt()) } if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) { // TODO: replace this // TODO: add toggle - color(0f, 1f, 0f, 0.7f) - line(particlePositions) + line(particlePositions, color = 0x80FFFFFF.toInt()) } } } @@ -124,8 +123,4 @@ object AncestralSpadeSolver : SubscriptionOwner { 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 index 6d87262..650e6f9 100644 --- a/src/main/kotlin/features/diana/DianaWaypoints.kt +++ b/src/main/kotlin/features/diana/DianaWaypoints.kt @@ -3,29 +3,29 @@ 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 +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig -object DianaWaypoints : FirmamentFeature { - override val identifier get() = "diana" - override val config get() = TConfig +object DianaWaypoints { + val identifier get() = "diana" - 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 } - } + @Config + 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 onBlockUse(event: UseBlockEvent) { + NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos) + } - @Subscribe - fun onBlockAttack(event: AttackBlockEvent) { - NearbyBurrowsSolver.onBlockClick(event.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 index 2fb4002..35af660 100644 --- a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt +++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt @@ -2,22 +2,20 @@ 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 net.minecraft.core.particles.ParticleTypes +import net.minecraft.core.BlockPos +import net.minecraft.util.Mth +import net.minecraft.core.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 { +object NearbyBurrowsSolver { private val recentlyDugBurrows: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(20) @@ -65,7 +63,7 @@ object NearbyBurrowsSolver : SubscriptionOwner { fun onParticles(event: ParticleSpawnEvent) { if (!DianaWaypoints.TConfig.nearbyWaypoints) return - val position: BlockPos = event.position.toBlockPos().down() + val position: BlockPos = event.position.toBlockPos().below() if (wasRecentlyDug(position)) return @@ -134,11 +132,8 @@ object NearbyBurrowsSolver : SubscriptionOwner { 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)) + return BlockPos(Mth.floor(x()), Mth.floor(y()), Mth.floor(z())) } diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt index 5151862..955d23b 100644 --- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt +++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt @@ -1,224 +1,224 @@ - 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 org.joml.Vector2i import kotlin.time.Duration.Companion.seconds -import net.minecraft.entity.passive.PigEntity -import net.minecraft.util.math.BlockPos +import net.minecraft.world.entity.animal.Pig +import net.minecraft.network.chat.Component +import net.minecraft.core.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.data.Config +import moe.nea.firmament.util.data.ManagedConfig 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) } - } +object AnniversaryFeatures { + val identifier: String + get() = "anniversary" - override val config: ManagedConfig? - get() = TConfig + @Config + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val enableShinyPigTracker by toggle("shiny-pigs") { true } + val trackPigCooldown by position("pig-hud", 200, 300) { Vector2i(100, 200) } + } - data class ClickedPig( + data class ClickedPig( val clickedAt: TimeMark, val startLocation: BlockPos, - val pigEntity: PigEntity - ) { - @Bind("timeLeft") - fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration - } - - val clickedPigs = ObservableList<ClickedPig>(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 (?<reward>.*) 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<Reward>() - - fun <T> ObservableList<T>.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 = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern() - val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern() - val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern() - fun parseReward(string: String): Reward { - expReward.useMatch<Unit>(string) { - val exp = parseShortNumber(group("exp")) - val kind = group("kind") - return Reward.EXP(exp, kind) - } - coinReward.useMatch<Unit>(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) - } - - @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<DisplayReward>(mutableListOf()) - - } + val pigEntity: Pig + ) { + @Bind("timeLeft") + fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration + } + + val clickedPigs = ObservableList<ClickedPig>(mutableListOf()) + var lastClickedPig: Pig? = null + + val pigDuration = 90.seconds + + @Subscribe + fun onTick(event: TickEvent) { + clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration } + } + + val pattern = "SHINY! You extracted (?<reward>.*) 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.blockPosition() ?: 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.distToCenterSqr(player.position) } ?: 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<Reward>() + + fun <T> ObservableList<T>.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 = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern() + val coinReward = "(?i)\\+(?<amount>$SHORT_NUMBER_FORMAT) Coins".toPattern() + val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern() + fun parseReward(string: String): Reward { + expReward.useMatch<Unit>(string) { + val exp = parseShortNumber(group("exp")) + val kind = group("kind") + return Reward.EXP(exp, kind) + } + coinReward.useMatch<Unit>(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 Pig) { + 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(): Component { + return when (backedBy) { + is Reward.Coins -> Component.literal("Coins") + is Reward.EXP -> Component.literal(backedBy.skill) + is Reward.Items -> itemStack.asImmutableItemStack().hoverName + is Reward.Unknown -> Component.literal(backedBy.text) + } + } + + @Bind + fun isKnown() = backedBy !is Reward.Unknown + } + + @get:Bind("rewards") + val rewards = ObservableList<DisplayReward>(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..9b87cc6 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.entity.player.Player +import net.minecraft.network.chat.Style +import net.minecraft.ChatFormatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + @Config + 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, ChatFormatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, ChatFormatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, ChatFormatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, ChatFormatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, ChatFormatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: ChatFormatting, + ) { + val searchedTextRgb = formatting.color!! + 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? Player ?: return + val cakeColor: Style = player.feedbackDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.value == 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 index 840fb8c..3f149ff 100644 --- a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt +++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt @@ -1,17 +1,16 @@ package moe.nea.firmament.features.events.carnival -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig -object CarnivalFeatures : FirmamentFeature { - object TConfig : ManagedConfig(identifier, Category.EVENTS) { +object CarnivalFeatures { + @Config + 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 + 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 index 1824225..64b3814 100644 --- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -2,17 +2,17 @@ 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.platform.MoulConfigPlatform 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 net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.Component +import net.minecraft.core.BlockPos +import net.minecraft.world.level.LevelAccessor import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.events.AttackBlockEvent @@ -45,7 +45,6 @@ object MinesweeperHelper { enum class Piece( - @get:Bind("fruitName") val fruitName: String, val points: Int, val specialAbility: String, @@ -118,24 +117,26 @@ object MinesweeperHelper { val textureUrl = "http://textures.minecraft.net/texture/$textureHash" val itemStack = createSkullItem(UUID.randomUUID(), textureUrl) .setSkyBlockFirmamentUiId("MINESWEEPER_$name") + @get:Bind("fruitName") + val textFruitName = Component.literal(fruitName) @Bind - fun getIcon() = ModernItemStack.of(itemStack) + fun getIcon() = MoulConfigPlatform.wrap(itemStack) - @Bind - fun pieceLabel() = fruitColor.formattingCode + fruitName + @get:Bind("pieceLabel") + val pieceLabel = Component.literal(fruitColor.formattingCode + fruitName) - @Bind - fun boardLabel() = "§a$totalPerBoard§7/§rboard" + @get:Bind("boardLabel") + val boardLabel = Component.literal("§a$totalPerBoard§7/§rboard") - @Bind("description") - fun getDescription() = buildString { + @get:Bind("description") + val getDescription = Component.literal(buildString { append(specialAbility) if (points >= 0) { append(" Default points: $points.") } } - } +) } object TutorialScreen { @get:Bind("pieces") @@ -158,7 +159,7 @@ object MinesweeperHelper { ; @Bind("itemType") - fun getItemStack() = ModernItemStack.of(ItemStack(itemType)) + fun getItemStack() = MoulConfigPlatform.wrap(ItemStack(itemType)) companion object { val id = SkyblockId("CARNIVAL_SHOVEL") @@ -175,10 +176,10 @@ object MinesweeperHelper { ) { 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 + fun getBlock(world: LevelAccessor) = world.getBlockState(toBlockPos()).block + fun isUnopened(world: LevelAccessor) = getBlock(world) == Blocks.SAND + fun isOpened(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE + fun isScorched(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE_STAIRS companion object { fun fromBlockPos(blockPos: BlockPos): BoardPosition? { @@ -221,8 +222,8 @@ object MinesweeperHelper { @Subscribe fun onChat(event: ProcessChatEvent) { if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) { - MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial")) + MC.sendChat(Component.translatable("firmament.carnival.tutorial.minesweeper").withStyle { + it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial")) }) } if (!CarnivalFeatures.TConfig.enableBombSolver) { @@ -259,7 +260,7 @@ object MinesweeperHelper { val boardPosition = BoardPosition.fromBlockPos(event.blockPos) log.log { "Breaking block at ${event.blockPos} ($boardPosition)" } gs.lastClickedPosition = boardPosition - gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack) + gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandItem) } @Subscribe @@ -267,7 +268,7 @@ object MinesweeperHelper { 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")) + this.text(pos.toBlockPos().above().center, Component.literal("§a$bombCount \uD83D\uDCA3")) } } } diff --git a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt deleted file mode 100644 index 76f6ed4..0000000 --- a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt +++ /dev/null @@ -1,41 +0,0 @@ -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>(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 index 7030319..ae4abd7 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -1,57 +1,72 @@ package moe.nea.firmament.features.fixes -import moe.nea.jarvis.api.Point +import org.joml.Vector2i 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 net.minecraft.client.Minecraft +import net.minecraft.client.KeyMapping +import net.minecraft.network.chat.Component 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr -object Fixes : FirmamentFeature { - override val identifier: String +object Fixes { + val identifier: String get() = "fixes" + @Config 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 autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } + val autoSprintUnderWater by toggle("auto-sprint-underwater") { true } + var autoSprintHudToggle by toggle("auto-sprint-hud-toggle") { true } + val autoSprintHud by position("auto-sprint-hud", 80, 10) { Vector2i() } val peekChat by keyBindingWithDefaultUnbound("peek-chat") + val peekChatScroll by toggle("peek-chat-scroll") { false } 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<Boolean> + keyBinding: KeyMapping, + cir: CallbackInfoReturnable<Boolean> ) { - if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true) - cir.returnValue = true + if (keyBinding !== Minecraft.getInstance().options.keySprint) return + if (!TConfig.autoSprint) return + val player = MC.player ?: return + if (player.isSprinting) return + if (!TConfig.autoSprintUnderWater && player.isInWater) 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, Text.translatable( - if (TConfig.autoSprint) - "firmament.fixes.auto-sprint.on" - else if (MC.player?.isSprinting == true) - "firmament.fixes.auto-sprint.sprinting" - else - "firmament.fixes.auto-sprint.not-sprinting" - ), 0, 0, -1, false + if (!TConfig.autoSprintKeyBinding.isBound || !TConfig.autoSprintHudToggle) return + it.context.pose().pushMatrix() + TConfig.autoSprintHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, ( + if (MC.player?.isSprinting == true) { + Component.translatable("firmament.fixes.auto-sprint.sprinting") + } else if (TConfig.autoSprint) { + if (!TConfig.autoSprintUnderWater && MC.player?.isInWater == true) + tr("firmament.fixes.auto-sprint.under-water", "In Water") + else + Component.translatable("firmament.fixes.auto-sprint.on") + } else { + Component.translatable("firmament.fixes.auto-sprint.not-sprinting") + } + ), 0, 0, -1, true ) - it.context.matrices.pop() + it.context.pose().popMatrix() } @Subscribe @@ -64,4 +79,8 @@ object Fixes : FirmamentFeature { fun shouldPeekChat(): Boolean { return TConfig.peekChat.isPressed(atLeast = true) } + + fun shouldScrollPeekedChat(): Boolean { + return TConfig.peekChatScroll + } } diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt new file mode 100644 index 0000000..edd511f --- /dev/null +++ b/src/main/kotlin/features/garden/HideComposterNoises.kt @@ -0,0 +1,34 @@ +package moe.nea.firmament.features.garden + +import net.minecraft.world.entity.animal.wolf.WolfSoundVariants +import net.minecraft.sounds.SoundEvent +import net.minecraft.sounds.SoundEvents +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object HideComposterNoises { + @Config + object TConfig : ManagedConfig("composter", Category.GARDEN) { + val hideComposterNoises by toggle("no-more-noises") { false } + } + + val composterSoundEvents: List<SoundEvent> = listOf( + SoundEvents.PISTON_EXTEND, + SoundEvents.WATER_AMBIENT, + SoundEvents.CHICKEN_EGG, + SoundEvents.WOLF_SOUNDS[WolfSoundVariants.SoundSet.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 index d2c79fd..5241f54 100644 --- a/src/main/kotlin/features/inventory/CraftingOverlay.kt +++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt @@ -1,20 +1,20 @@ 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 net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.item.ItemStack +import net.minecraft.ChatFormatting 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 { +object CraftingOverlay { - private var screen: GenericContainerScreen? = null + private var screen: ContainerScreen? = null private var recipe: NEUCraftingRecipe? = null private var useNextScreen = false private val craftingOverlayIndices = listOf( @@ -24,7 +24,7 @@ object CraftingOverlay : FirmamentFeature { ) val CRAFTING_SCREEN_NAME = "Craft Item" - fun setOverlay(screen: GenericContainerScreen?, recipe: NEUCraftingRecipe) { + fun setOverlay(screen: ContainerScreen?, recipe: NEUCraftingRecipe) { this.screen = screen if (screen == null) { useNextScreen = true @@ -34,7 +34,7 @@ object CraftingOverlay : FirmamentFeature { @Subscribe fun onScreenChange(event: ScreenChangeEvent) { - if (useNextScreen && event.new is GenericContainerScreen + if (useNextScreen && event.new is ContainerScreen && event.new.title?.string == "Craft Item" ) { useNextScreen = false @@ -42,18 +42,19 @@ object CraftingOverlay : FirmamentFeature { } } - override val identifier: String + 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 (slot.container != screen?.menu?.container) return + val recipeIndex = craftingOverlayIndices.indexOf(slot.containerSlot) if (recipeIndex < 0) return val expectedItem = recipe.inputs[recipeIndex] - val actualStack = slot.stack ?: ItemStack.EMPTY!! + val actualStack = slot.item ?: ItemStack.EMPTY!! val actualEntry = SBItemStack(actualStack) if ((actualEntry.skyblockId != expectedItem.skyblockId || actualEntry.getStackSize() < expectedItem.amount) && expectedItem.amount.toInt() != 0 @@ -66,15 +67,15 @@ object CraftingOverlay : FirmamentFeature { 0x80FF0000.toInt() ) } - if (!slot.hasStack()) { + if (!slot.hasItem()) { val itemStack = SBItemStack(expectedItem)?.asImmutableItemStack() ?: return - event.context.drawItem(itemStack, event.slot.x, event.slot.y) - event.context.drawStackOverlay( + event.context.renderItem(itemStack, event.slot.x, event.slot.y) + event.context.renderItemDecorations( MC.font, itemStack, event.slot.x, event.slot.y, - "${Formatting.RED}${expectedItem.amount.toInt()}" + "${ChatFormatting.RED}${expectedItem.amount.toInt()}" ) } } diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt index 4aa8202..e9d0631 100644 --- a/src/main/kotlin/features/inventory/ItemHotkeys.kt +++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt @@ -2,22 +2,26 @@ 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName object ItemHotkeys { + @Config 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)) { @@ -26,7 +30,7 @@ object ItemHotkeys { var item = event.screen.focusedItemStack ?: return val skyblockId = item.skyBlockId ?: return item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item - if (HypixelStaticData.hasBazaarStock(skyblockId)) { + if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) { MC.sendCommand("bz ${item.getSearchName()}") } else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) { MC.sendCommand("ahs ${item.getSearchName()}") diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index fdc378a..9712067 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -1,45 +1,38 @@ 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 net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.skyblock.Rarity -import moe.nea.firmament.util.unformattedString -object ItemRarityCosmetics : FirmamentFeature { - override val identifier: String +object ItemRarityCosmetics { + val identifier: String get() = "item-rarity-cosmetics" + @Config 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!!) + val c = Color(it.value.color!!) c.rgb } - fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) { + fun drawItemStackRarity(drawContext: GuiGraphics, 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"), + drawContext.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:item_rarity_background"), x, y, 16, 16, rgb @@ -50,7 +43,7 @@ object ItemRarityCosmetics : FirmamentFeature { @Subscribe fun onRenderSlot(it: SlotRenderEvents.Before) { if (!TConfig.showItemRarityBackground) return - val stack = it.slot.stack ?: return + val stack = it.slot.item ?: return drawItemStackRarity(it.context, it.slot.x, it.slot.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..15bdcfa --- /dev/null +++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt @@ -0,0 +1,30 @@ +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.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName +import moe.nea.firmament.util.useMatch + +object JunkHighlighter { + val identifier: String + get() = "junk-highlighter" + + @Config + 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.item.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 index 5ca10f7..e0bb4b1 100644 --- a/src/main/kotlin/features/inventory/PetFeatures.kt +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -1,40 +1,561 @@ package moe.nea.firmament.features.inventory -import net.minecraft.util.Identifier +import java.util.regex.Matcher +import org.joml.Vector2i +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.util.StringRepresentable +import moe.nea.firmament.Firmament 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.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.jarvis.JarvisIntegration +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.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.SkyBlockIsland +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.formattedString +import moe.nea.firmament.util.parseShortNumber 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.skyblock.TabListAPI +import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.titleCase +import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor -object PetFeatures : FirmamentFeature { - override val identifier: String +object PetFeatures { + val identifier: String get() = "pets" - override val config: ManagedConfig? - get() = TConfig - + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val highlightEquippedPet by toggle("highlight-pet") { true } + val petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { + Vector2i() + } + val petOverlayHudStyle by choice("pet-overlay-hud-style") { PetOverlayHudStyles.PLAIN_NO_BACKGROUND } } - val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + enum class PetOverlayHudStyles : StringRepresentable { + PLAIN_NO_BACKGROUND, + COLOUR_NO_BACKGROUND, + PLAIN_BACKGROUND, + COLOUR_BACKGROUND, + ICON_ONLY; + + override fun getSerializedName() : String { + return name + } + } + + private val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + private val autopetPattern = + "§cAutopet §eequipped your §7\\[Lvl (\\d{1,3})\\] §([fa956d])([\\w\\s]+)§e! §aVIEW RULE".toPattern() + private val petItemPattern = "§aYour pet is now holding (§[fa956d][\\w\\s]+)§a.".toPattern() + private val petLevelUpPattern = "§aYour §([fa956d])([\\w\\s]+) §aleveled up to level §9(\\d+)§a!".toPattern() + private val petMap = HashMap<String, ParsedPet>() + private var currentPetUUID: String = "" + private var tempTabPet: ParsedPet? = null + private var tempChatPet: ParsedPet? = null + + @Subscribe + fun onProfileSwitch(event: ProfileSwitchEvent) { + petMap.clear() + currentPetUUID = "" + tempTabPet = null + tempChatPet = 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) { + // Cache pets + petMenuTitle.useMatch(MC.screenName ?: return) { + val stack = event.slot.item + if (!stack.isEmpty) cachePet(stack) + if (stack.petData?.active == true) { + if (currentPetUUID == "") currentPetUUID = stack.skyblockUUID.toString() + // Highlight active pet feature + if (!TConfig.highlightEquippedPet) return event.context.drawGuiTexture( - event.slot.x, event.slot.y, 0, 16, 16, - Identifier.of("firmament:selected_pet_background") + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, ) } + } + } + + private fun cachePet(stack: ItemStack) { + // Cache information about a pet + if (stack.skyblockUUID == null) return + if (petMap.containsKey(stack.skyblockUUID.toString()) && + petMap[stack.skyblockUUID.toString()]?.isComplete == true) return + + val pet = PetParser.parsePetMenuSlot(stack) ?: return + petMap[stack.skyblockUUID.toString()] = pet + } + + @Subscribe + fun onSlotClick(event: SlotClickEvent) { + // Check for switching/removing pet manually + petMenuTitle.useMatch(MC.screenName ?: return) { + if (event.slot.container is Inventory) return + if (event.button != 0 && event.button != 1) return + val petData = event.stack.petData ?: return + if (petData.active == true) { + currentPetUUID = "None" + return + } + if (event.button != 0) return + if (!petMap.containsKey(event.stack.skyblockUUID.toString())) cachePet(event.stack) + currentPetUUID = event.stack.skyblockUUID.toString() + } } + @Subscribe + fun onChatEvent(event: ProcessChatEvent) { + // Handle AutoPet + var matcher = autopetPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == matcher.group(3) && + pet.rarity == PetParser.reversePetColourMap[matcher.group(2)] && + pet.level <= matcher.group(1).toInt() + } + if (tempMap.isNotEmpty()) { + currentPetUUID = tempMap.keys.first() + } else { + tempChatPet = PetParser.parsePetChatMessage(matcher.group(3), matcher.group(2), matcher.group(1).toInt()) + currentPetUUID = "" + } + tempTabPet = null + return + } + // Handle changing pet item + // This is needed for when pet item can't be found in tab list + matcher = petItemPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + petMap[currentPetUUID]?.petItem = matcher.group(1) + tempTabPet?.petItem = matcher.group(1) + tempChatPet?.petItem = matcher.group(1) + // TODO: Handle tier boost pet items if required + // I'm not rich enough to be able to test tier boosts + } + // Handle pet levelling up + // This is needed for when pet level can't be found in tab list + matcher = petLevelUpPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempPet = + PetParser.parsePetChatMessage(matcher.group(2), matcher.group(1), matcher.group(3).toInt()) ?: return + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == tempPet.name && + pet.rarity == tempPet.rarity && + pet.level <= tempPet.level + } + if (tempMap.isNotEmpty()) petMap[tempMap.keys.first()]?.update(tempPet) + if (tempTabPet?.name == tempPet.name && tempTabPet?.rarity == tempPet.rarity) { + tempTabPet?.update(tempPet) + } + if (tempChatPet?.name == tempPet.name && tempChatPet?.rarity == tempPet.rarity) tempChatPet?.update(tempPet) + } + } + private fun renderLinesAndBackground(it: HudRenderEvent, lines: List<Component>) { + // Render background for the hud + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + var maxWidth = 0 + lines.forEach { if (MC.font.width(it) > maxWidth) maxWidth = MC.font.width(it.unformattedString) } + val height = if (MC.font.lineHeight * lines.size > 32) MC.font.lineHeight * lines.size else 32 + it.context.fill(0, -3, 40 + maxWidth, height + 2, 0x80000000.toInt()) + } + + // Render text for the hud + lines.forEachIndexed { index, line -> + it.context.drawString( + MC.font, + line.copy().withColor(ChatFormatting.GRAY), + 36, + MC.font.lineHeight * index, + -1, + true + ) + } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay || !SBData.isOnSkyblock) return + + // Possibly handle Montezuma as a future feature? Could track how many pieces have been found etc + // Would likely need to be a separate config toggle though since that has + // very different usefulness/purpose to the pet hud outside of rift + if (SBData.skyblockLocation == SkyBlockIsland.RIFT) return + + // Initial data + var pet: ParsedPet? = null + // Do not render the HUD if there is no pet active + if (currentPetUUID == "None") return + // Get active pet from cache + if (currentPetUUID != "") pet = petMap[currentPetUUID] + // Parse tab widget for pet data + val tabPet = PetParser.parseTabWidget(TabListAPI.getWidgetLines(TabListAPI.WidgetName.PET)) + if (pet == null && tabPet == null && tempTabPet == null && tempChatPet == null) { + // No data on current pet + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + val lines = mutableListOf<Component>() + lines.add(Component.literal("" + ChatFormatting.WHITE + "Unknown Pet")) + lines.add(Component.literal("Open Pets Menu To Fix")) + renderLinesAndBackground(it, lines) + it.context.pose().popMatrix() + return + } + if (pet == null) { + // Pet is only known through tab widget or chat message, potentially saved from tab widget elsewhere + // (e.g. another server or before removing the widget from the tab list) + pet = tabPet ?: tempTabPet ?: tempChatPet ?: return + if (tempTabPet == null) tempTabPet = tabPet + } + + // Update pet based on tab widget if needed + if (tabPet != null && pet.name == tabPet.name && pet.rarity == tabPet.rarity) { + if (tabPet.level > pet.level) { + // Level has increased since caching + pet.level = tabPet.level + pet.currentExp = tabPet.currentExp + pet.expForNextLevel = tabPet.expForNextLevel + pet.totalExp = tabPet.totalExp + } else if (tabPet.currentExp > pet.currentExp) { + // Exp has increased since caching, level has not + pet.currentExp = tabPet.currentExp + pet.totalExp = tabPet.totalExp + } + if (tabPet.petItem != pet.petItem && tabPet.petItem != "Unknown") { + // Pet item has changed since caching + pet.petItem = tabPet.petItem + pet.petItemStack = tabPet.petItemStack + } + } + + // Set the text for the HUD + + val lines = mutableListOf<Component>() + + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + // Colour Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForNextLevel)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L100: ${shortFormat(pet.totalExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForMax)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "" + ChatFormatting.AQUA + ChatFormatting.BOLD + "MAX LEVEL" + )) + lines.add(Component.literal( + "" + ChatFormatting.GOLD + "+" + ChatFormatting.YELLOW + "${shortFormat(pet.overflowExp)} XP" + )) + } + } else if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND) { + // Plain Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(if (pet.petItem != "None" && pet.petItem != "Unknown") + pet.petItem.substring(2) else pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}/" + + "${shortFormat(pet.expForNextLevel)} " + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "Required L100: ${shortFormat(pet.totalExp)}/${shortFormat(pet.expForMax)} " + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "MAX LEVEL" + )) + lines.add(Component.literal( + "+${shortFormat(pet.overflowExp)} XP" + )) + } + } + + // Render HUD + + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + + renderLinesAndBackground(it, lines) + + // Draw the ItemStack + it.context.pose().pushMatrix() + it.context.pose().translate(-0.5F, -0.5F) + it.context.pose().scale(2f, 2f) + it.context.renderItem(pet.petItemStack.value, 0, 0) + it.context.pose().popMatrix() + + it.context.pose().popMatrix() + } +} + +object PetParser { + private val petNamePattern = " §7\\[Lvl (\\d{1,3})] §([fa956d])([\\w\\s]+)".toPattern() + private val petItemPattern = " (§[fa956dbc4][\\s\\w]+)".toPattern() + private val petExpPattern = " §e((?:\\d{1,3}[,.]?)+\\d*[kM]?)§6\\/§e((?:\\d{1,3}[,.]?)+\\d*[kM]?) XP §6\\(\\d+(?:.\\d+)?%\\)".toPattern() + private val petOverflowExpPattern = " §6\\+§e((?:\\d{1,3}[,.])+\\d*[kM]?) XP".toPattern() + private val katPattern = " Kat:.*".toPattern() + + val reversePetColourMap = mapOf( + "f" to Rarity.COMMON, + "a" to Rarity.UNCOMMON, + "9" to Rarity.RARE, + "5" to Rarity.EPIC, + "6" to Rarity.LEGENDARY, + "d" to Rarity.MYTHIC + ) + + val found = HashMap<String, Matcher>() + + @OptIn(ExpensiveItemCacheApi::class) + fun parsePetChatMessage(name: String, rarityCode: String, level: Int) : ParsedPet? { + val petId = name.uppercase().replace(" ", "_") + val petRarity = reversePetColourMap[rarityCode] ?: Rarity.COMMON + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(level + 1).toDouble() - + expLadder.getPetExpForLevel(level).toDouble() + } + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(level).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + return ParsedPet( + name, + petRarity, + level, + -1, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + 0.0, + "Unknown", + petItemStack, + false + ) + } + + @OptIn(ExpensiveItemCacheApi::class) + fun parseTabWidget(lines: List<Component>): ParsedPet? { + found.clear() + for (line in lines.reversed()) { + if (!found.containsKey("kat")) { + val matcher = katPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["kat"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["exp"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petOverflowExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["overflow"] = matcher + continue + } + } + if (!found.containsKey("item")) { + val matcher = petItemPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["item"] = matcher + continue + } + } + if (!found.containsKey("name")) { + val matcher = petNamePattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["name"] = matcher + continue + } + } + } + if (!found.containsKey("name")) return null + + val petName = titleCase(found.getValue("name").group(3)) + val petRarity = reversePetColourMap.getValue(found.getValue("name").group(2)) + val petId = petName.uppercase().replace(" ", "_") + + val petLevel = found.getValue("name").group(1).toInt() + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(petLevel + 1).toDouble() - + expLadder.getPetExpForLevel(petLevel).toDouble() + } + + val overflowExp: Double = if (found.containsKey("overflow")) + parseShortNumber(found.getValue("overflow").group(1)) else 0.0 + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(petLevel).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + + var petItem = "Unknown" + if (found.containsKey("item")) { + petItem = found.getValue("item").group(1) + } + + return ParsedPet( + petName, + petRarity, + petLevel, + maxLevel, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + overflowExp, + petItem, + petItemStack, + false + ) + } + + fun parsePetMenuSlot(stack: ItemStack) : ParsedPet? { + val petData = stack.petData ?: return null + val expData = petData.level + val overflow = if (expData.expTotal - expData.expRequiredForMaxLevel > 0) + (expData.expTotal - expData.expRequiredForMaxLevel).toDouble() else 0.0 + val petItem = if (stack.petData?.heldItem != null) + RepoManager.neuRepo.items.items.getValue(stack.petData?.heldItem).displayName else "None" + return ParsedPet( + titleCase(petData.type), + Rarity.fromNeuRepo(petData.tier) ?: Rarity.COMMON, + expData.currentLevel, + expData.maxLevel, + ExpLadders.getExpLadder(petData.skyblockId.toString(), petData.tier), + expData.expInCurrentLevel.toDouble(), + expData.expRequiredForNextLevel.toDouble(), + expData.expTotal.toDouble(), + expData.expTotal.toDouble() - expData.expInCurrentLevel.toDouble(), + expData.expRequiredForMaxLevel.toDouble(), + overflow, + petItem, + lazy { stack }, + true + ) + } +} + +data class ParsedPet( + val name: String, + val rarity: Rarity, + var level: Int, + val maxLevel: Int, + val expLadder: ExpLadders.ExpLadder?, + var currentExp: Double, + var expForNextLevel: Double, + var totalExp: Double, + var totalExpBeforeLevel: Double, + val expForMax: Double, + var overflowExp: Double, + var petItem: String, + var petItemStack: Lazy<ItemStack>, + var isComplete: Boolean +) { + fun update(other: ParsedPet) { + // Update the pet data to reflect another instance (of itself) + if (other.level > level) { + level = other.level + currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + totalExp = other.totalExp + totalExpBeforeLevel = other.totalExpBeforeLevel + overflowExp = other.overflowExp + } else { + if (other.currentExp > currentExp) currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + if (other.totalExp > totalExp) totalExp = other.totalExp + if (other.totalExpBeforeLevel > totalExpBeforeLevel) totalExpBeforeLevel = other.totalExpBeforeLevel + if (other.overflowExp > overflowExp) overflowExp = other.overflowExp + } + if (other.petItem != "Unknown") petItem = other.petItem + isComplete = false + } } diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt index 4477203..54802db 100644 --- a/src/main/kotlin/features/inventory/PriceData.kt +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -1,51 +1,120 @@ - - package moe.nea.firmament.features.inventory -import net.minecraft.text.Text +import org.lwjgl.glfw.GLFW +import net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable 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 +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.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.getLogicalStackSize +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 { + val identifier: String + get() = "price-data" + + @Config + 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 + } + } -object PriceData : FirmamentFeature { - override val identifier: String - get() = "price-data" + enum class AvgLowestBin : StringRepresentable { + OFF, + ONEDAYAVGLOWESTBIN, + THREEDAYAVGLOWESTBIN, + SEVENDAYAVGLOWESTBIN; - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val tooltipEnabled by toggle("enable-always") { true } - val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") - } + override fun getSerializedName(): String { + return name + } + } - override val config get() = TConfig + fun formatPrice(label: Component, price: Double): Component { + return Component.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Component.literal(formatCommas(price, fractionalDigits = 1)) + .append(if (price != 1.0) " coins" else " coin") + .gold() + .bold() + ) + } - @Subscribe - fun onItemTooltip(it: ItemTooltipEvent) { - if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { - return - } - val sbId = it.stack.skyBlockId - val bazaarData = HypixelStaticData.bazaarData[sbId] - val lowestBin = HypixelStaticData.lowestBin[sbId] - if (bazaarData != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1)) - ) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1)) - ) - } else if (lowestBin != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin", - FirmFormatters.formatCommas(lowestBin, 1)) - ) - } - } + @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.getLogicalStackSize() + 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(Component.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(Component.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 index 1e9b1b8..e508016 100644 --- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -1,12 +1,13 @@ 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 net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute @@ -30,27 +31,28 @@ object REIDependencyWarner { var sentWarning = false fun modrinthLink(slug: String) = - "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric" + "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getCurrentVersion().name()}&l=fabric" - fun downloadButton(modName: String, modId: String, slug: String): Text { + fun downloadButton(modName: String, modId: String, slug: String): Component { val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId) - return Text.literal(" - ") + return Component.literal(" - ") .white() - .append(Text.literal("[").aqua()) - .append(Text.translatable("firmament.download", modName) - .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) } + .append(Component.literal("[").aqua()) + .append(Component.translatable("firmament.download", modName) + .withStyle { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } .yellow() .also { if (alreadyDownloaded) - it.append(Text.translatable("firmament.download.already", modName) + it.append(Component.translatable("firmament.download.already", modName) .lime()) }) - .append(Text.literal("]").aqua()) + .append(Component.literal("]").aqua()) } @Subscribe fun checkREIDependency(event: SkyblockServerUpdateEvent) { if (!SBData.isOnSkyblock) return + if (!RepoManager.TConfig.warnForMissingItemListMod) return if (hasREI) return if (sentWarning) return sentWarning = true @@ -58,11 +60,11 @@ object REIDependencyWarner { 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") + Component.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") + .append(Component.translatable("firmament.reiwarning.disable") .clickCommand("/firm disablereiwarning") .grey()) ) @@ -74,9 +76,9 @@ object REIDependencyWarner { if (hasREI) return event.subcommand("disablereiwarning") { thenExecute { - RepoManager.Config.warnForMissingItemListMod = false - RepoManager.Config.save() - MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow()) + RepoManager.TConfig.warnForMissingItemListMod = false + RepoManager.TConfig.markDirty() + MC.sendChat(Component.translatable("firmament.reiwarning.disabled").yellow()) } } } diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt index c47867b..c492a75 100644 --- a/src/main/kotlin/features/inventory/SaveCursorPosition.kt +++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt @@ -1,66 +1,63 @@ - - package moe.nea.firmament.features.inventory +import org.lwjgl.glfw.GLFW 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 com.mojang.blaze3d.platform.InputConstants 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<Double, Double>? = null - var savedPosition: SavedPosition? = null - - data class SavedPosition( - val middle: Pair<Double, Double>, - val cursor: Pair<Double, Double>, - val savedAt: TimeMark = TimeMark.now() - ) - - @JvmStatic - fun saveCursorOriginal(positionedX: Double, positionedY: Double) { - savedPositionedP1 = Pair(positionedX, positionedY) - } - - @JvmStatic - fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { - 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) - } +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object SaveCursorPosition { + val identifier: String + get() = "save-cursor-position" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + var savedPositionedP1: Pair<Double, Double>? = null + var savedPosition: SavedPosition? = null + + data class SavedPosition( + val middle: Pair<Double, Double>, + val cursor: Pair<Double, Double>, + val savedAt: TimeMark = TimeMark.now() + ) + + @JvmStatic + fun saveCursorOriginal(positionedX: Double, positionedY: Double) { + savedPositionedP1 = Pair(positionedX, positionedY) + } + + @JvmStatic + fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { + 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 + ) { + InputConstants.grabOrReleaseMouse( + MC.window, + InputConstants.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 index 99130d5..fca40c8 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,107 +4,197 @@ 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.entity.player.PlayerInventory -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 net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.ChestMenu +import net.minecraft.world.inventory.InventoryMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientInitEvent 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.InputModifiers 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.accessors.castAccessor +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig 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.render.GuiRenderLayers +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 +object SlotLocking { + val identifier: String get() = "slot-locking" @Serializable - data class Data( + data class DimensionData( val lockedSlots: MutableSet<Int> = mutableSetOf(), - val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), + val boundSlots: BoundSlots = BoundSlots(), + ) + + @Serializable + data class Data( val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val rift: DimensionData = DimensionData(), + val overworld: DimensionData = DimensionData(), + ) + + + val currentWorldData + get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT) + DConfig.data.rift + else + DConfig.data.overworld + + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, ) + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet<BoundSlot> = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List<BoundSlot> { + 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<BoundSlots> { + override val descriptor: SerialDescriptor + get() = serializer<JsonElement>().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer<MutableSet<BoundSlot>>() + .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<MutableSet<BoundSlot>>(), json)) + + } + } + } + + + @Config 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) + SavedKeyBinding.keyWithMods(GLFW.GLFW_KEY_L, InputModifiers.of(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 slotBindOnlyInInv by toggle("bind-only-in-inv") { false } + 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 { + enum class SlotRenderLinesMode : StringRepresentable { EVERYTHING, ONLY_BOXES, NOTHING; - override fun asString(): String { + override fun getSerializedName(): String { return name } } - override val config: TConfig - get() = TConfig - + @Config object DConfig : ProfileSpecificDataHolder<Data>(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 - } + get() = currentWorldData?.lockedSlots - fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { + fun isSalvageScreen(screen: AbstractContainerScreen<*>?): Boolean { if (screen == null) return false return screen.title.unformattedString.contains("Salvage Item") } - fun isTradeScreen(screen: HandledScreen<*>?): Boolean { + fun isTradeScreen(screen: AbstractContainerScreen<*>?): 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) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val middlePane = handler.container.getItem(handler.container.containerSize - 5) if (middlePane == null) return false return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" } - fun isNpcShop(screen: HandledScreen<*>?): Boolean { + fun isNpcShop(screen: AbstractContainerScreen<*>?): 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) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val sellItem = handler.container.getItem(handler.container.containerSize - 5) if (sellItem == null) return false if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true val lore = sellItem.loreAccordingToNbt @@ -114,13 +204,19 @@ object SlotLocking : FirmamentFeature { @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 + if (!event.slot.hasItem()) return + if (event.slot.item.displayNameAccordingToNbt.unformattedString != "Salvage Items") return + val inv = event.slot.container var anyBlocked = false - for (i in 0 until event.slot.index) { - val stack = inv.getStack(i) - if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + for (i in 0 until event.slot.containerSlot) { + val stack = inv.getItem(i) + if (IsSlotProtectedEvent.shouldBlockInteraction( + null, + ClickType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack + ) + ) anyBlocked = true } if (anyBlocked) { @@ -130,46 +226,69 @@ object SlotLocking : FirmamentFeature { @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 doesNotDeleteItem = event.actionType == ClickType.SWAP + || event.actionType == ClickType.PICKUP + || event.actionType == ClickType.QUICK_MOVE + || event.actionType == ClickType.QUICK_CRAFT + || event.actionType == ClickType.CLONE + || event.actionType == ClickType.PICKUP_ALL val isSellOrTradeScreen = isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) - if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) + if ((!isSellOrTradeScreen || event.slot?.container !is Inventory) && 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())) { + if (it.slot != null && it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf())) { it.protect() } } @Subscribe + fun onEvent(event: ClientInitEvent) { + 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 ?: mapOf() + val boundSlots = currentWorldData?.boundSlots ?: BoundSlots() val isValidAction = - it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) + it.actionType == ClickType.QUICK_MOVE || (it.actionType == ClickType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return - val handler = MC.handledScreen?.screenHandler ?: return + val handler = MC.handledScreen?.menu ?: return + if (TConfig.slotBindOnlyInInv && handler !is InventoryMenu) + return val slot = it.slot - if (slot != null && it.slot.inventory is PlayerInventory) { - val boundSlot = boundSlots.entries.find { - it.value == slot.index || it.key == slot.index - } ?: return + if (slot != null && it.slot.container is Inventory) { + val matchingSlots = boundSlots.findMatchingSlots(slot.containerSlot) + if (matchingSlots.isEmpty()) return it.protectSilent() - val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) - inventorySlot?.swapWithHotBar(handler, boundSlot.key) + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @@ -177,10 +296,25 @@ object SlotLocking : FirmamentFeature { fun onLockUUID(it: HandledScreenKeyPressedEvent) { if (!it.matches(TConfig.lockUUID)) return val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - val stack = slot.stack ?: return + val stack = slot.item ?: 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) { @@ -197,7 +331,7 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament val storedSlot = storedLockingSlot ?: return @@ -205,13 +339,11 @@ object SlotLocking : FirmamentFeature { 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.entries.removeIf { - it.value == invSlot.index - } - boundSlots[hotBarSlot.index] = invSlot.index + val boundSlots = currentWorldData?.boundSlots ?: return + lockedSlots?.remove(hotBarSlot.containerSlot) + lockedSlots?.remove(invSlot.containerSlot) + boundSlots.removeDuplicateForInventory(invSlot.containerSlot) + boundSlots.insert(hotBarSlot.containerSlot, invSlot.containerSlot) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -223,61 +355,75 @@ object SlotLocking : FirmamentFeature { } if (it.matches(TConfig.slotBind)) { storedLockingSlot = null - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.containerSlot) } } @Subscribe fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) { - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true) - val accScreen = event.screen as AccessorHandledScreen + val accScreen = event.screen.castAccessor() val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - for (it in boundSlots.entries) { - val hotbarSlot = findByIndex(it.key) ?: continue - val inventorySlot = findByIndex(it.value) ?: continue + val highlitSlots = mutableSetOf<Slot>() + 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 + || accScreen.focusedSlot_Firmament === inventorySlot if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) continue - val color = if (anyHovered) - me.shedaniel.math.Color.ofOpaque(0x00FF00) - else - me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) + 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 + color(anyHovered) ) - event.context.drawBorder(hotbarSlot.x + sx, - hotbarSlot.y + sy, - 16, 16, color.color) - event.context.drawBorder(inventorySlot.x + sx, - inventorySlot.y + sy, - 16, 16, color.color) + event.context.submitOutline( + hotbarSlot.x + sx, + hotbarSlot.y + sy, + 16, 16, color(hotbarSlot in highlitSlots).color + ) + event.context.submitOutline( // TODO: 1.21.10 + 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 accScreen = event.screen.castAccessor() val hoveredSlot = accScreen.focusedSlot_Firmament - ?.takeIf { it.inventory is PlayerInventory } + ?.takeIf { it.container is Inventory } ?.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()) + event.context.submitOutline( + draggingSlot.x + sx, + draggingSlot.y + sy, + 16, + 16, + 0xFF00FF00u.toInt() + ) // TODO: 1.21.10 if (hoveredSlot == null) { event.context.drawLine( borderX + sx, borderY + sy, @@ -291,9 +437,11 @@ object SlotLocking : FirmamentFeature { hovX + sx, hovY + sy, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) - event.context.drawBorder(hoveredSlot.x + sx, - hoveredSlot.y + sy, - 16, 16, 0xFF00FF00u.toInt()) + event.context.submitOutline( + hoveredSlot.x + sx, + hoveredSlot.y + sy, + 16, 16, 0xFF00FF00u.toInt() + ) } } @@ -301,13 +449,13 @@ object SlotLocking : FirmamentFeature { return if (isHotbar()) { x + 9 to y } else { - x + 9 to y + 17 + x + 9 to y + 16 } } fun Slot.isHotbar(): Boolean { - return index < 9 + return containerSlot < 9 } @Subscribe @@ -319,16 +467,14 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() - if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + val boundSlots = currentWorldData?.boundSlots ?: BoundSlots() + if (slot.container is Inventory) { + if (boundSlots.removeAllInvolving(slot.containerSlot)) { // intentionally do nothing - } else if (slot.index in lockedSlots) { - lockedSlots.remove(slot.index) + } else if (slot.containerSlot in lockedSlots) { + lockedSlots.remove(slot.containerSlot) } else { - lockedSlots.add(slot.index) + lockedSlots.add(slot.containerSlot) } DConfig.markDirty() CommonSoundEffects.playSuccess() @@ -338,10 +484,10 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlot(it: HandledScreenKeyPressedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - if (slot.inventory !is PlayerInventory) return + if (slot.container !is Inventory) return if (it.matches(TConfig.slotBind)) { storedLockingSlot = storedLockingSlot ?: slot return @@ -354,17 +500,17 @@ object SlotLocking : FirmamentFeature { @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()) + val isSlotLocked = it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf()) + val isUUIDLocked = (it.slot.item?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { - it.context.drawGuiTexture( - GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, + it.context.blitSprite( + RenderPipelines.GUI_TEXTURED, when { isSlotLocked -> - (Identifier.of("firmament:slot_locked")) + (ResourceLocation.parse("firmament:slot_locked")) isUUIDLocked -> - (Identifier.of("firmament:uuid_locked")) + (ResourceLocation.parse("firmament:uuid_locked")) else -> error("unreachable") diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..9bb78c9 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -7,25 +7,29 @@ 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 net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig 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 { + @Config 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 { + enum class TimerFormat(val formatter: DateTimeFormatter) : StringRepresentable { RFC(DateTimeFormatter.RFC_1123_DATE_TIME), LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), SOCIALIST( @@ -45,6 +49,7 @@ object TimerInLore { 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) @@ -52,7 +57,7 @@ object TimerInLore { constructor(format: String) : this(DateTimeFormatter.ofPattern(format)) - override fun asString(): String { + override fun getSerializedName(): String { return name } } @@ -80,13 +85,25 @@ object TimerInLore { COMMUNITYPROJECTS("Contribute again", "Come back at"), CHOCOLATEFACTORY("Next Charge", "Available at"), STONKSAUCTION("Auction ends in", "Ends at"), - LIZSTONKREDEMPTION("Resets in:", "Resets 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)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[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 @@ -107,9 +124,13 @@ object TimerInLore { 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()) + event.lines.add( + i + 1, + tr( + "firmament.loretimer.missingrelative", + "Found a relative countdown with no baseline (Firmament)" + ).grey() + ) continue } baseLine = lastTimer @@ -119,10 +140,11 @@ object TimerInLore { 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()) + event.lines.add( + i + 1, + Component.literal("${countdownType.label}: ") + .grey() + .append(Component.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..8d4760b --- /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.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton + +object WardrobeKeybinds { + @Config + object TConfig : ManagedConfig("wardrobe-keybinds", 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 } + } + val allowUnequipping by toggle("allow-unequipping") { true } + } + + 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 + if (event.screen !is AbstractContainerScreen<*>) 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.menu + 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.item.item == Items.ARROW) { + previousSlot.clickLeftMouseButton(handler) + } else if (nextPressed && nextSlot.item.item == Items.ARROW) { + nextSlot.clickLeftMouseButton(handler) + } + } + + + val slot = + slotKeybindsWithSlot + .find { event.matches(it.second.get()) } + ?.first ?: return + + event.cancel() + + val handler = event.screen.menu + val invSlot = handler.getSlot(slot) + + val itemStack = invSlot.item + val isSelected = itemStack.item == Items.LIME_DYE + val isSelectable = itemStack.item == Items.PINK_DYE + if (!isSelectable && !isSelected) return + if (!TConfig.allowUnequipping && isSelected) return + + invSlot.clickLeftMouseButton(handler) + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt index a46bd76..0cb51ca 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory.buttons import com.mojang.brigadier.StringReader @@ -7,80 +5,120 @@ 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 net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.commands.CommandBuildContext +import net.minecraft.commands.arguments.item.ItemArgument +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.flag.FeatureFlags +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.repo.ExpensiveItemCacheApi 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.ErrorUtil 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? = "", + var x: Int, + var y: Int, + var anchorRight: Boolean, + var anchorBottom: Boolean, + var icon: String? = "", + var command: String? = "", + var isGigantic: Boolean = false, ) { - 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) - fun getItemForName0(icon: String): ItemStack { - val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) - var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) - if (repoItem == null) { - 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) - } + val myDimension get() = if (isGigantic) bigDimension else dimensions + + companion object { + val itemStackParser by lazy { + ItemArgument.item( + CommandBuildContext.simple( + MC.defaultRegistries, + FeatureFlags.VANILLA_SET + ) + ) + } + val dimensions = Dimension(18, 18) + val gap = 2 + val bigDimension = Dimension(dimensions.width * 2 + gap, dimensions.height * 2 + gap) + 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)).createItemStack(1, false) + }.getOrNull() + if (componentItem != null) + itemStack = componentItem + } + } + } + if (itemStack.item == Items.PAINTING) + ErrorUtil.logError("created broken itemstack for inventory button $icon: $itemStack") + return itemStack + } + } + + fun render(context: GuiGraphics) { + context.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:inventory_button_background"), + 0, + 0, + myDimension.width, + myDimension.height, + ) + if (isGigantic) { + context.pose().pushMatrix() + context.pose().translate(myDimension.width / 2F, myDimension.height / 2F) + context.pose().scale(2F) + context.renderItem(getItem(), -8, -8) + context.pose().popMatrix() + } else { + context.renderItem(getItem(), 1, 1) + } + } - fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() + 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 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 getBounds(guiRect: Rectangle): Rectangle { + return Rectangle(getPosition(guiRect), myDimension) + } - fun getItem(): ItemStack { - return getItemForName(icon ?: "") - } + 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 index ee3ae8b..6b6a2d6 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -1,17 +1,24 @@ package moe.nea.firmament.features.inventory.buttons import io.github.notenoughupdates.moulconfig.common.IItemStack -import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext 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.gui.DrawContext -import net.minecraft.client.gui.widget.ButtonWidget -import net.minecraft.client.util.InputUtil -import net.minecraft.text.Text -import net.minecraft.util.math.MathHelper -import net.minecraft.util.math.Vec2f +import net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.MultiLineTextWidget +import net.minecraft.client.gui.components.StringWidget +import net.minecraft.client.input.KeyEvent +import com.mojang.blaze3d.platform.InputConstants +import net.minecraft.network.chat.Component +import net.minecraft.util.Mth +import net.minecraft.world.phys.Vec2 import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.FragmentGuiScreen import moe.nea.firmament.util.MC @@ -28,10 +35,13 @@ class InventoryButtonEditor( @field:Bind var icon: String = originalButton.icon ?: "" + @field:Bind + var isGigantic: Boolean = originalButton.isGigantic + @Bind fun getItemIcon(): IItemStack { save() - return ModernItemStack.of(InventoryButton.getItemForName(icon)) + return MoulConfigPlatform.wrap(InventoryButton.getItemForName(icon)) } @Bind @@ -42,6 +52,7 @@ class InventoryButtonEditor( fun save() { originalButton.icon = icon + originalButton.isGigantic = isGigantic originalButton.command = command } } @@ -49,30 +60,92 @@ class InventoryButtonEditor( var buttons: MutableList<InventoryButton> = InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList() - override fun close() { + override fun onClose() { InventoryButtons.DConfig.data.buttons = buttons InventoryButtons.DConfig.markDirty() - super.close() + super.onClose() + } + + override fun resize(client: Minecraft, width: Int, height: Int) { + lastGuiRect.move( + MC.window.guiScaledWidth / 2 - lastGuiRect.width / 2, + MC.window.guiScaledHeight / 2 - lastGuiRect.height / 2 + ) + super.resize(client, width, height) } override fun init() { super.init() - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) { + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 25, + Component.translatable("firmament.inventory-buttons.delete"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 40, + Component.translatable("firmament.inventory-buttons.info"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.reset")) { + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 10) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.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) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 35) .width(lastGuiRect.width - 20) .build() ) - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) { + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.save-preset")) { ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons)) } - .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.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("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 85) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.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("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 110) .width(lastGuiRect.width - 20) .build() ) @@ -83,14 +156,20 @@ class InventoryButtonEditor( val movedButtons = mutableListOf<InventoryButton>() 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 - )) + 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) } @@ -99,9 +178,11 @@ class InventoryButtonEditor( 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) + 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) @@ -114,35 +195,48 @@ class InventoryButtonEditor( return newButtons } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + context.pose().pushMatrix() + PanelComponent.DefaultBackgroundRenderer.VANILLA + .render( + MoulConfigRenderContext(context), + lastGuiRect.minX, lastGuiRect.minY, + lastGuiRect.width, lastGuiRect.height, + ) + context.pose().popMatrix() super.render(context, mouseX, mouseY, delta) - context.matrices.push() - context.matrices.translate(0f, 0f, -10f) - context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1) - 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) + context.pose().pushMatrix() + context.pose().translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat()) button.render(context) - context.matrices.pop() + context.pose().popMatrix() } + renderPopup(context, mouseX, mouseY, delta) } - 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() + override fun keyPressed(input: KeyEvent): Boolean { + if (super.keyPressed(input)) return true + if (input.input() == GLFW.GLFW_KEY_ESCAPE) { + onClose() 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)) } + override fun mouseReleased(click: MouseButtonEvent): Boolean { + if (super.mouseReleased(click)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(click.x, click.y)) } if (clickedButton != null && !justPerformedAClickAction) { - createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY)) + if (InputConstants.isKeyDown( + MC.window, + InputConstants.KEY_LCONTROL + ) + ) Editor(clickedButton).delete() + else createPopup( + MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), + Point(click.x, click.y) + ) return true } justPerformedAClickAction = false @@ -150,19 +244,27 @@ class InventoryButtonEditor( 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 + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + if (super.mouseDragged(click, offsetX, offsetY)) return true - if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) { - initialDragMousePosition = Vec2f(-10F, -10F) + if (initialDragMousePosition.distanceToSqr(Vec2(click.x.toFloat(), click.y.toFloat())) >= 4 * 4) { + initialDragMousePosition = Vec2(-10F, -10F) lastDraggedButton?.let { dragging -> justPerformedAClickAction = true - val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt()) + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(click.x.toInt(), click.y.toInt()) ?: return true dragging.x = offsetX dragging.y = offsetY dragging.anchorRight = anchorRight dragging.anchorBottom = anchorBottom + if (!anchorRight && offsetX > -dragging.myDimension.width + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.x = -dragging.myDimension.width + if (!anchorRight && offsetY > -dragging.myDimension.height + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.y = -dragging.myDimension.height } } return false @@ -170,7 +272,7 @@ class InventoryButtonEditor( var lastDraggedButton: InventoryButton? = null var justPerformedAClickAction = false - var initialDragMousePosition = Vec2f(-10F, -10F) + var initialDragMousePosition = Vec2(-10F, -10F) data class AnchoredCoords( val anchorRight: Boolean, @@ -180,35 +282,30 @@ class InventoryButtonEditor( ) fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? { - if (lastGuiRect.contains(mx, my) || lastGuiRect.contains( - Point( - mx + InventoryButton.dimensions.width, - my + InventoryButton.dimensions.height, - ) - ) - ) return null - 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 + if (InputConstants.isKeyDown(MC.window, InputConstants.KEY_LSHIFT)) { + offsetX = Mth.floor(offsetX / 20F) * 20 + offsetY = Mth.floor(offsetY / 20F) * 20 } - return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + 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)) } + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + if (super.mouseClicked(click, doubled)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(click.x, click.y) } if (clickedButton != null) { lastDraggedButton = clickedButton - initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat()) + initialDragMousePosition = Vec2(click.y.toFloat(), click.y.toFloat()) return true } - val mx = mouseX.toInt() - val my = mouseY.toInt() + val mx = click.x.toInt() + val my = click.y.toInt() val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null)) justPerformedAClickAction = true diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt index d282157..c6ad14d 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt @@ -1,7 +1,6 @@ package moe.nea.firmament.features.inventory.buttons -import kotlinx.serialization.encodeToString -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC @@ -18,7 +17,7 @@ object InventoryButtonTemplates { ErrorUtil.catch<InventoryButton?>("Could not import button") { Firmament.json.decodeFromString<InventoryButton>(it).also { if (it.icon?.startsWith("extra:") == true) { - MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed")) + MC.sendChat(Component.translatable("firmament.inventory-buttons.import-failed")) } } }.or { diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index d5b5417..eaa6138 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -1,88 +1,118 @@ - - 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.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.network.chat.Component 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.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.impl.v1.FirmamentAPIImpl import moe.nea.firmament.util.MC import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.accessors.getProperRectangle +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder -import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.gold + +object InventoryButtons { + + @Config + 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 InventoryButtons : FirmamentFeature { - override val identifier: String - get() = "inventory-buttons" + @Config + object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data) - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val _openEditor by button("open-editor") { - openEditor() - } - } + @Serializable + data class Data( + var buttons: MutableList<InventoryButton> = mutableListOf() + ) - object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) + fun getValidButtons(screen: AbstractContainerScreen<*>): Sequence<InventoryButton> { + if (TConfig.onlyInv && screen !is InventoryScreen) return emptySequence() + if (FirmamentAPIImpl.extensions.any { it.shouldHideInventoryButtons(screen) }) { + return emptySequence() + } + return DConfig.data.buttons.asSequence().filter(InventoryButton::isValid) + } - @Serializable - data class Data( - var buttons: MutableList<InventoryButton> = mutableListOf() - ) + @Subscribe + fun onRectangles(it: HandledScreenPushREIEvent) { + val bounds = it.screen.getProperRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.block(buttonBounds) + } + } - override val config: ManagedConfig - get() = TConfig + @Subscribe + fun onClickScreen(it: HandledScreenClickEvent) { + val bounds = it.screen.getProperRectangle() + 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 + } + } + } - fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() } + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() - @Subscribe - fun onRectangles(it: HandledScreenPushREIEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - val buttonBounds = button.getBounds(bounds) - it.block(buttonBounds) - } - } + @Subscribe + fun onRenderForeground(it: HandledScreenForegroundEvent) { + val bounds = it.screen.getProperRectangle() - @Subscribe - fun onClickScreen(it: HandledScreenClickEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - val buttonBounds = button.getBounds(bounds) - if (buttonBounds.contains(it.mouseX, it.mouseY)) { - MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) - break - } - } - } + var hoveredComponent: InventoryButton? = null + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.context.pose().pushMatrix() + it.context.pose().translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat()) + button.render(it.context) + it.context.pose().popMatrix() - @Subscribe - fun onRenderForeground(it: HandledScreenForegroundEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - 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() - } - lastRectangle = bounds - } + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.setComponentTooltipForNextFrame( + MC.font, + listOf(Component.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 - 100, - MC.window.scaledHeight / 2 - 100, - 200, 200, - ) - ) - ) - } + var lastRectangle: Rectangle? = null + fun openEditor() { + ScreenUtil.setScreenLater( + InventoryButtonEditor( + lastRectangle ?: Rectangle( + MC.window.guiScaledWidth / 2 - 88, + MC.window.guiScaledHeight / 2 - 83, + 176, 166, + ) + ) + ) + } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt index 8fad4df..964f415 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -4,9 +4,9 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.inventory.ChestMenu import moe.nea.firmament.util.ifMatches import moe.nea.firmament.util.unformattedString @@ -16,24 +16,24 @@ import moe.nea.firmament.util.unformattedString sealed interface StorageBackingHandle { sealed interface HasBackingScreen { - val handler: GenericContainerScreenHandler + val handler: ChestMenu } /** * 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 + data class Overview(override val handler: ChestMenu) : 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 <index + 1>` for enderchest pages. */ - data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) : + data class Page(override val handler: ChestMenu, 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() + 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 @@ -46,13 +46,13 @@ sealed interface StorageBackingHandle { returnsNotNull() implies (screen != null) } if (screen == null) return null - if (screen !is GenericContainerScreen) return null + if (screen !is ContainerScreen) return null val title = screen.title.unformattedString - if (title == "Storage") return Overview(screen.screenHandler) + if (title == "Storage") return Overview(screen.menu) return title.ifMatches(enderChestName) { - Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) } ?: title.ifMatches(backPackName) { - Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) } } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt index 2e807de..7f96637 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -1,59 +1,106 @@ 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 net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Items +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ChestInventoryUpdateEvent 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.async.discard import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder -object StorageOverlay : FirmamentFeature { - +object StorageOverlay { + @Config object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData) - override val identifier: String + val identifier: String get() = "storage-overlay" + @Config 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 showInactivePageTooltips by toggle("inactive-page-tooltips") { false } 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.item ?: 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) + fun onChestContentUpdate(event: ChestInventoryUpdateEvent) { + rememberContent(currentHandler) } @Subscribe fun onClick(event: SlotClickEvent) { - if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9 + if (lastStorageOverlay != null && event.slot.container !is Inventory && event.slot.containerSlot < 9 && event.stack.item != Items.BLACK_STAINED_GLASS_PANE ) { skipNextStorageOverlayBackflip = true @@ -64,18 +111,18 @@ object StorageOverlay : FirmamentFeature { 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 + ?: ((it.old as? AbstractContainerScreen<*>)?.customGui as? StorageOverlayCustom)?.overview var storageOverviewScreen = it.old as? StorageOverviewScreen - val screen = it.new as? GenericContainerScreen + val screen = it.new as? ContainerScreen + rememberContent(currentHandler) 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 + player?.connection?.send(ServerboundContainerClosePacket(oldHandler.handler.containerId)) + if (player?.containerMenu === oldHandler.handler) { + player.containerMenu = player.inventoryMenu } } storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay @@ -100,25 +147,24 @@ object StorageOverlay : FirmamentFeature { screen.customGui = StorageOverlayCustom( currentHandler ?: return, screen, - storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)) + 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 + val data = Data.data.storageInventories when (handler) { is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data) is StorageBackingHandle.Page -> rememberPage(handler, data) } - Data.markDirty() } private fun rememberStorageOverview( handler: StorageBackingHandle.Overview, data: SortedMap<StoragePageSlot, StorageData.StorageInventory> ) { - for ((index, stack) in handler.handler.stacks.withIndex()) { + for ((index, stack) in handler.handler.items.withIndex()) { // TODO: replace with slot iteration // Ignore unloaded item stacks if (stack.isEmpty) continue val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue @@ -132,15 +178,15 @@ object StorageOverlay : FirmamentFeature { data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null) } } + Data.markDirty() } private fun rememberPage( handler: StorageBackingHandle.Page, data: SortedMap<StoragePageSlot, StorageData.StorageInventory> ) { - // 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() }) + VirtualInventory(handler.handler.items.take(handler.handler.rowCount * 9).drop(9).map { it.copy() }) data.compute(handler.storagePageSlot) { slot, existingInventory -> (existingInventory ?: StorageData.StorageInventory( slot.defaultName(), @@ -150,5 +196,6 @@ object StorageOverlay : FirmamentFeature { it.inventory = newStacks } } + Data.markDirty(newStacks.serializationCache.discard()) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 6092e26..98e8085 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -2,21 +2,27 @@ 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 net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.inventory.Slot import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.accessors.castAccessor import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack class StorageOverlayCustom( - val handler: StorageBackingHandle, - val screen: GenericContainerScreen, - val overview: StorageOverlayScreen, + val handler: StorageBackingHandle, + val screen: ContainerScreen, + val overview: StorageOverlayScreen, ) : CustomGui() { override fun onVoluntaryExit(): Boolean { overview.isExiting = true + StorageOverlayScreen.resetScroll() return super.onVoluntaryExit() } @@ -24,20 +30,20 @@ class StorageOverlayCustom( return overview.getBounds() } - override fun afterSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun afterSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) context.disableScissor() } - override fun beforeSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun beforeSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) overview.createScissors(context) } override fun onInit() { - overview.init(MinecraftClient.getInstance(), screen.width, screen.height) + overview.init(Minecraft.getInstance(), screen.width, screen.height) overview.init() - screen as AccessorHandledScreen + screen.castAccessor() screen.x_Firmament = overview.measurements.x screen.y_Firmament = overview.measurements.y screen.backgroundWidth_Firmament = overview.measurements.totalWidth @@ -47,7 +53,7 @@ class StorageOverlayCustom( 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 (slot.container !is Inventory) { if (!overview.getScrollPanelInner().contains(pointX, pointY)) return false } @@ -58,48 +64,50 @@ class StorageOverlayCustom( return false } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseReleased(mouseX, mouseY, button) + override fun mouseReleased(click: MouseButtonEvent): Boolean { + return overview.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { - return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + return overview.mouseDragged(click, offsetX, offsetY) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyReleased(keyCode, scanCode, modifiers) + override fun keyReleased(input: KeyEvent): Boolean { + return overview.keyReleased(input) } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyPressed(keyCode, scanCode, modifiers) + override fun keyPressed(input: KeyEvent): Boolean { + return overview.keyPressed(input) } - override fun charTyped(chr: Char, modifiers: Int): Boolean { - return overview.charTyped(chr, modifiers) + override fun charTyped(input: CharacterEvent): Boolean { + return overview.charTyped(input) } - override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) + override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { + return overview.mouseClicked(click, doubled, (handler as? StorageBackingHandle.Page)?.storagePageSlot) } - override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + override fun render(drawContext: GuiGraphics, 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.drawPages( + drawContext, + mouseX, + mouseY, + delta, + (handler as? StorageBackingHandle.Page)?.storagePageSlot, + screen.menu.slots.take(screen.menu.rowCount * 9).drop(9), + Point((screen.castAccessor()).x_Firmament, screen.y_Firmament) + ) overview.drawScrollBar(drawContext) overview.drawControls(drawContext, mouseX, mouseY) } override fun moveSlot(slot: Slot) { - val index = slot.index + val index = slot.containerSlot if (index in 0..<36) { val (x, y) = overview.getPlayerInventorySlotPosition(index) - slot.x = x - (screen as AccessorHandledScreen).x_Firmament + slot.x = x - (screen.castAccessor()).x_Firmament slot.y = y - screen.y_Firmament } else { slot.x = -100000 @@ -113,6 +121,8 @@ class StorageOverlayCustom( 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 index cf1cf1d..d56e4d2 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -13,13 +13,17 @@ 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 net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.gui.EmptyComponent import moe.nea.firmament.gui.FirmButtonComponent @@ -35,10 +39,11 @@ 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("")) { +class StorageOverlayScreen : Screen(Component.literal("")) { companion object { val PLAYER_WIDTH = 184 @@ -46,19 +51,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val PLAYER_Y_INSET = 3 val SLOT_SIZE = 18 val PADDING = 10 - val PAGE_WIDTH = SLOT_SIZE * 9 + 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 + PLAYER_Y_INSET - val CONTROL_HEIGHT = 100 + 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 scroll: Float = 0F var pageWidthCount = StorageOverlay.TConfig.columns inner class Measurements { @@ -67,20 +81,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val x = width / 2 - overviewWidth / 2 val overviewHeight = minOf( height - PLAYER_HEIGHT - minOf(80, height / 10), - StorageOverlay.TConfig.height) + 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 = x - CONTROL_WIDTH - val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2 + 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() - var lastRenderedInnerHeight = 0 public override fun init() { super.init() pageWidthCount = StorageOverlay.TConfig.columns @@ -99,6 +113,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat()) return true } + fun coerceScroll(offset: Float) { scroll = (scroll + offset) .coerceAtMost(getMaxScroll()) @@ -107,19 +122,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { 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") + val playerInventorySprite = ResourceLocation.parse("firmament:storageoverlay/player_inventory") + val upperBackgroundSprite = ResourceLocation.parse("firmament:storageoverlay/upper_background") + val slotRowSprite = ResourceLocation.parse("firmament:storageoverlay/storage_row") + val scrollbarBackground = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_background") + val scrollbarKnob = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_knob") + val controllerBackground = ResourceLocation.parse("firmament:storageoverlay/storage_controls") - override fun close() { + override fun onClose() { isExiting = true - super.close() + resetScroll() + super.onClose() } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) drawBackgrounds(context) drawPages(context, mouseX, mouseY, delta, null, null, Point()) @@ -132,7 +148,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return scroll / getMaxScroll() } - fun drawScrollBar(context: DrawContext) { + fun drawScrollBar(context: GuiGraphics) { val sbRect = getScrollBarRect() context.drawGuiTexture( scrollbarBackground, @@ -148,21 +164,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun editPages() { isExiting = true - val hs = MC.screen as? HandledScreen<*> - if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { - hs.customGui = null - } else { - MC.sendCommand("storage") + MC.instance.schedule { + val hs = MC.screen as? AbstractContainerScreen<*> + 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 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 searchField = TextFieldComponent( + searchText, 100, GetSetter.constant(true), + tr("firmament.storage-overlay.search.suggestion", "Search...").string, + IMinecraft.INSTANCE.defaultFontRenderer + ) val controlComponent = PanelComponent( ColumnComponent( searchField, @@ -180,30 +204,36 @@ class StorageOverlayScreen : Screen(Text.literal("")) { guiContext.adopt(controlComponent) } - fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) { + fun drawControls(context: GuiGraphics, mouseX: Int, mouseY: Int) { context.drawGuiTexture( controllerBackground, measurements.controlX, measurements.controlY, - CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT) + CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT + ) context.drawMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX, mouseY) + 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 drawBackgrounds(context: GuiGraphics) { + 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<Int, Int> { @@ -216,55 +246,63 @@ class StorageOverlayScreen : Screen(Text.literal("")) { ) } - fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - val items = MC.player?.inventory?.main ?: return + fun drawPlayerInventory(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + val items = MC.player?.inventory?.nonEquipmentItems ?: return items.withIndex().forEach { (index, item) -> val (x, y) = getPlayerInventorySlotPosition(index) - context.drawItem(item, x, y, 0) - context.drawStackOverlay(textRenderer, item, x, y) + context.renderItem(item, x, y, 0) + context.renderItemDecorations(font, item, x, y) } } fun getScrollBarRect(): Rectangle { - return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, - measurements.y + PADDING, - SCROLL_BAR_WIDTH, - measurements.innerScrollPanelHeight) + 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) + return Rectangle( + measurements.x + PADDING, + measurements.y + PADDING, + measurements.innerScrollPanelWidth, + measurements.innerScrollPanelHeight + ) } - fun createScissors(context: DrawContext) { + fun createScissors(context: GuiGraphics) { val rect = getScrollPanelInner() - context.enableScissor( - rect.minX, rect.minY, - rect.maxX, rect.maxY + 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<Slot>?, - slotOffset: Point + context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float, + excluding: StoragePageSlot?, + slots: List<Slot>?, + 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 + drawPage( + context, + rect.x, + rect.y, + page, inventory, + if (excluding == page) slots else null, + slotOffset, + mouseX, + mouseY ) } context.disableScissor() + } @@ -272,40 +310,45 @@ class StorageOverlayScreen : Screen(Text.literal("")) { 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 mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + return mouseClicked(click, doubled, null) } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseReleased(click: MouseButtonEvent): 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)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + click.x.toInt(), click.y.toInt(), + MouseEvent.Click(click.button(), false) + ) ) return true - return super.mouseReleased(mouseX, mouseY, button) + return super.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { if (knobGrabbed) { val sbRect = getScrollBarRect() - val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + val percentage = (click.x - 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) + return super.mouseDragged(click, offsetX, offsetY) } - fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + fun mouseClicked(click: MouseButtonEvent, doubled: Boolean, 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. + val mouseX = click.x + val mouseY = click.y if (getScrollPanelInner().contains(mouseX, mouseY)) { - val data = StorageOverlay.Data.data ?: StorageData() + val data = StorageOverlay.Data.data layoutedForEach(data) { rect, page, _ -> - if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) { + if (rect.contains(mouseX, mouseY) && activePage != page && click.button() == 0) { page.navigateTo() return true } @@ -320,52 +363,58 @@ class StorageOverlayScreen : Screen(Text.literal("")) { knobGrabbed = true return true } - if (clickMCComponentInPlace(controlComponent, - measurements.controlX, measurements.controlY, - CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX.toInt(), mouseY.toInt(), - MouseEvent.Click(button, true)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX.toInt(), mouseY.toInt(), + MouseEvent.Click(click.button(), true) + ) ) return true return false } - override fun charTyped(chr: Char, modifiers: Int): Boolean { + override fun charTyped(input: CharacterEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.CharTyped(chr) + KeyboardEvent.CharTyped(input.codepointAsString().first()) // TODO: i dont like this .first() ) ) { return true } - return super.charTyped(chr, modifiers) + return super.charTyped(input) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyReleased(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, false) + KeyboardEvent.KeyPressed(input.input(), input.scancode, false) ) ) { return true } - return super.keyReleased(keyCode, scanCode, modifiers) + return super.keyReleased(input) + } + + 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 { + override fun keyPressed(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, true) + KeyboardEvent.KeyPressed(input.input(), input.scancode, true) ) ) { return true } - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } @@ -414,7 +463,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val filter = getFilteredPages() for ((page, inventory) in data.storageInventories.entries) { if (page !in filter) continue - val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight } + val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + font.lineHeight } ?: 18 maxHeight = maxOf(maxHeight, currentHeight) val rect = Rectangle( @@ -435,61 +484,102 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun drawPage( - context: DrawContext, - x: Int, - y: Int, - page: StoragePageSlot, - inventory: StorageData.StorageInventory, - slots: List<Slot>?, - slotOffset: Point, + context: GuiGraphics, + x: Int, + y: Int, + page: StoragePageSlot, + inventory: StorageData.StorageInventory, + slots: List<Slot>?, + slotOffset: Point, + mouseX: Int, + mouseY: Int, ): 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) + context.drawString( + font, + Component.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() - context.drawText(textRenderer, Text.literal(name), x + 4, y + 2, - if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true) - context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE) + val name = inventory.title + val pageHeight = inv.rows * SLOT_SIZE + 8 + font.lineHeight + if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage) + context.submitOutline( + x, + y + 3 + font.lineHeight, + PAGE_WIDTH, + inv.rows * SLOT_SIZE + 4, + StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB() + ) + context.drawString( + font, Component.literal(name), x + 6, y + 3, + if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true + ) + context.drawGuiTexture( + slotRowSprite, + x + 2, + y + 5 + font.lineHeight, + PAGE_SLOTS_WIDTH, + inv.rows * SLOT_SIZE + ) + val scrollPanel = getScrollPanelInner() inv.stacks.forEachIndexed { index, stack -> - val slotX = (index % 9) * SLOT_SIZE + x + 1 - val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1 - val fakeSlot = FakeSlot(stack, slotX, slotY) + val slotX = (index % 9) * SLOT_SIZE + x + 3 + val slotY = (index / 9) * SLOT_SIZE + y + 5 + font.lineHeight + 1 if (slots == null) { + val fakeSlot = FakeSlot(stack, slotX, slotY) SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot)) - context.drawItem(stack, slotX, slotY) - context.drawStackOverlay(textRenderer, stack, slotX, slotY) + context.renderItem(stack, slotX, slotY) + context.renderItemDecorations(font, stack, slotX, slotY) SlotRenderEvents.After.publish(SlotRenderEvents.After(context, fakeSlot)) + if (StorageOverlay.TConfig.showInactivePageTooltips && !stack.isEmpty && + mouseX >= slotX && mouseY >= slotY && + mouseX <= slotX + 16 && mouseY <= slotY + 16 && + scrollPanel.contains(mouseX, mouseY)) { + try { + context.setTooltipForNextFrame(font, stack, mouseX, mouseY) + } catch (e: IllegalStateException) { + context.setComponentTooltipForNextFrame(font, listOf(Component.nullToEmpty(ChatFormatting.RED.toString() + + "Error Getting Tooltip!"), Component.nullToEmpty(ChatFormatting.YELLOW.toString() + + "Open page to fix" + ChatFormatting.RESET)), mouseX, mouseY) + } + } } else { val slot = slots[index] slot.x = slotX - slotOffset.x slot.y = slotY - slotOffset.y } } - return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight + return pageHeight + 6 } fun getBounds(): List<Rectangle> { 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)) + 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 index 9112fab..3c40fc6 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt @@ -4,17 +4,19 @@ 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 net.minecraft.world.level.block.Blocks +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.Item +import net.minecraft.world.item.Items +import net.minecraft.network.chat.Component +import net.minecraft.world.item.DyeColor import moe.nea.firmament.util.MC import moe.nea.firmament.util.toShedaniel -class StorageOverviewScreen() : Screen(Text.empty()) { +class StorageOverviewScreen() : Screen(Component.empty()) { companion object { val emptyStorageSlotItems = listOf<Item>( Blocks.RED_STAINED_GLASS_PANE.asItem(), @@ -22,39 +24,49 @@ class StorageOverviewScreen() : Screen(Text.empty()) { Items.GRAY_DYE ) val pageWidth get() = 19 * 9 + + var scroll = 0 + var lastRenderedHeight = 0 } val content = StorageOverlay.Data.data ?: StorageData() var isClosing = false - var scroll = 0 - var lastRenderedHeight = 0 + override fun init() { + super.init() + scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0) + } + + override fun onClose() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0 + super.onClose() + } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, 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) + context.pose().pushMatrix() + context.pose().translate(offsetX.toFloat(), offsetY.toFloat()) renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY) - context.matrices.pop() + context.pose().popMatrix() } } inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) { var offsetY = 0 - var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll + var currentMaxHeight = StorageOverlay.TConfig.margin - StorageOverlay.TConfig.padding - scroll var totalHeight = -currentMaxHeight content.storageInventories.onEachIndexed { index, (key, value) -> - val pageX = (index % StorageOverlay.config.columns) + val pageX = (index % StorageOverlay.TConfig.columns) if (pageX == 0) { - currentMaxHeight += StorageOverlay.config.padding + currentMaxHeight += StorageOverlay.TConfig.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) + width / 2 - (StorageOverlay.TConfig.columns * (pageWidth + StorageOverlay.TConfig.padding) - StorageOverlay.TConfig.padding) / 2 + pageX * (pageWidth + StorageOverlay.TConfig.padding) onEach(Pair(key, value), xPosition, offsetY) val height = getStorePageHeight(value) currentMaxHeight = max(currentMaxHeight, height) @@ -62,22 +74,22 @@ class StorageOverviewScreen() : Screen(Text.empty()) { lastRenderedHeight = totalHeight + currentMaxHeight } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { layoutedForEach { (k, p), x, y -> - val rx = mouseX - x - val ry = mouseY - y + val rx = click.x - x + val ry = click.y - y if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) { - close() + onClose() StorageOverlay.lastStorageOverlay = this k.navigateTo() return true } } - return super.mouseClicked(mouseX, mouseY, button) + return super.mouseClicked(click, doubled) } fun getStorePageHeight(page: StorageData.StorageInventory): Int { - return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60 + return page.inventory?.rows?.let { it * 19 + MC.font.lineHeight + 2 } ?: 60 } override fun mouseScrolled( @@ -88,36 +100,38 @@ class StorageOverviewScreen() : Screen(Text.empty()) { ): Boolean { scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt() - .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0) + .coerceAtMost(getMaxScroll()).coerceAtLeast(0) return true } - private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { - context.drawText(MC.font, page.title, 2, 2, -1, true) + private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.TConfig.margin + + private fun renderStoragePage(context: GuiGraphics, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { + context.drawString(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) + context.drawCenteredString(MC.font, Component.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 + val y = (index / 9) * 19 + MC.font.lineHeight + 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) + context.renderItem(stack, x + 1, y + 1) + context.renderItemDecorations(MC.font, stack, x + 1, y + 1) } } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) + override fun keyPressed(input: KeyEvent): Boolean { + if (input.input() == GLFW.GLFW_KEY_ESCAPE) isClosing = true - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index 3b86184..69d686f 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -1,9 +1,10 @@ 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 java.util.Base64 +import java.util.concurrent.CompletableFuture +import kotlinx.coroutines.async import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -11,13 +12,16 @@ 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.nbt.NbtCompound +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtIo -import net.minecraft.nbt.NbtList +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtSizeTracker -import net.minecraft.registry.RegistryOps +import net.minecraft.nbt.NbtAccounter +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.inventory.storageoverlay.VirtualInventory.Serializer.writeToByteArray +import moe.nea.firmament.util.Base64Util import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.mc.TolerantRegistriesOps @@ -28,6 +32,10 @@ data class VirtualInventory( ) { val rows = stacks.size / 9 + val serializationCache = CompletableFuture.supplyAsync { + writeToByteArray(this) + } + init { assert(stacks.size % 9 == 0) assert(stacks.size / 9 in 1..5) @@ -35,41 +43,47 @@ data class VirtualInventory( object Serializer : KSerializer<VirtualInventory> { + fun writeToByteArray(value: VirtualInventory): ByteArray { + val list = ListTag() + val ops = getOps() + value.stacks.forEach { + if (it.isEmpty) list.add(CompoundTag()) + else list.add(ErrorUtil.catch("Could not serialize item") { + ItemStack.CODEC.encode( + it, + ops, + CompoundTag() + ).orThrow + } + .or { CompoundTag() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(CompoundTag().also { it.put(INVENTORY, list) }, baos) + return baos.toByteArray() + } + 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, NbtCompound.COMPOUND_TYPE.toInt()) + val n = NbtIo.readCompressed(ByteArrayInputStream(Base64Util.decodeBytes(s)), NbtAccounter.create(100_000_000)) + val items = n.getList(INVENTORY).getOrNull() val ops = getOps() - return VirtualInventory(items.map { - it as NbtCompound + return VirtualInventory(items?.map { + it as CompoundTag 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) + fun getOps() = MC.currentOrDefaultRegistryNbtOps 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()) + encoder.encodeString(Base64Util.encodeToString(value.serializationCache.get())) } } } diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt new file mode 100644 index 0000000..cc58f8a --- /dev/null +++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt @@ -0,0 +1,139 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.util.LinkedList +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object BlockZapperOverlay { + val identifier: String + get() = "block-zapper-overlay" + + @Config + 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") + } + + val bannedZapper: List<Block> = listOf<Block>( + 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<BlockPos> = 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.level ?: return + val heldItem = MC.stackInHand + if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + val hitResult = MC.instance.hitResult ?: return + + val zapperBlocks: HashSet<BlockPos> = HashSet() + val returnablePositions = LinkedList<BlockPos>() + + 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.above()).block + if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) { + var i = 0 + while (i < 164) { + zapperBlocks.add(pos) + returnablePositions.remove(pos) + + val availableNeighbors: MutableList<BlockPos> = ArrayList() + + for (offset in zapperOffsets) { + val newPos = pos.offset(offset) + + if (zapperBlocks.contains(newPos)) continue + + val state: BlockState? = world.getBlockState(newPos) + if (state != null && state.block === block) { + val above = newPos.above() + 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?.isShiftKeyDown ?: 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..3f16922 --- /dev/null +++ b/src/main/kotlin/features/items/BonemerangOverlay.kt @@ -0,0 +1,94 @@ +package moe.nea.firmament.features.items + +import me.shedaniel.math.Color +import org.joml.Vector2i +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.entity.player.Player +import net.minecraft.ChatFormatting +import net.minecraft.world.phys.AABB +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +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 { + val identifier: String + get() = "bonemerang-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var bonemerangOverlay by toggle("bonemerang-overlay") { false } + val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() } + var highlightHitEntities by toggle("highlight-hit-entities") { false } + } + + fun getEntities(): MutableSet<LivingEntity> { + val entities = mutableSetOf<LivingEntity>() + val camera = MC.camera as? Player ?: return entities + val player = MC.player ?: return entities + val world = player.level ?: return entities + + val cameraPos = camera.eyePosition + val rayDirection = camera.lookAngle.normalize() + val endPos = cameraPos.add(rayDirection.scale(15.0)) + val foundEntities = world.getEntities(camera, AABB(cameraPos, endPos).inflate(1.0)) + + for (entity in foundEntities) { + if (entity !is LivingEntity || entity is ArmorStand || entity.isInvisible) continue + val hitResult = entity.boundingBox.inflate(0.35).clip(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(ChatFormatting.BLUE.color!!)) + } + + 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.pose().pushMatrix() + TConfig.bonemerangOverlayHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, String.format( + tr( + "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s" + ).string, entities.size + ), 0, 0, -1, true + ) + it.context.pose().popMatrix() + } +} diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt new file mode 100644 index 0000000..a59fcbd --- /dev/null +++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt @@ -0,0 +1,234 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import net.minecraft.world.level.block.Blocks +import net.minecraft.core.Holder +import net.minecraft.tags.BlockTags +import net.minecraft.tags.TagKey +import net.minecraft.network.chat.Component +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import net.minecraft.world.phys.shapes.Shapes +import net.minecraft.world.level.BlockGetter +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +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 +import moe.nea.firmament.util.tr + +object EtherwarpOverlay { + val identifier: String + get() = "etherwarp-overlay" + + @Config + 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) } + val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) } + val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) } + val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) } + var wireframe by toggle("wireframe") { false } + var failureText by toggle("failure-text") { false } + } + + enum class EtherwarpResult(val label: Component?, val color: () -> ChromaColour) { + SUCCESS(null, TConfig::cubeColour), + INTERACTION_BLOCKED( + tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"), + TConfig::tooCloseCubeColour + ), + TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour), + OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour), + } + + val interactionBlocked = Checker( + setOf( + Blocks.HOPPER, + Blocks.CHEST, + Blocks.ENDER_CHEST, + Blocks.FURNACE, + Blocks.CRAFTING_TABLE, + Blocks.CAULDRON, + Blocks.WATER_CAULDRON, + Blocks.ENCHANTING_TABLE, + Blocks.DISPENSER, + Blocks.DROPPER, + Blocks.BREWING_STAND, + Blocks.TRAPPED_CHEST, + Blocks.LEVER, + ), + setOf( + BlockTags.DOORS, + BlockTags.TRAPDOORS, + BlockTags.ANVIL, + BlockTags.FENCE_GATES, + ) + ) + + data class Checker<T>( + val direct: Set<T>, + val byTag: Set<TagKey<T>>, + ) { + fun matches(entry: Holder<T>): Boolean { + return entry.value() in direct || checkTags(entry, byTag) + } + } + + val etherwarpHallpasses = Checker( + setOf( + Blocks.CREEPER_HEAD, + Blocks.CREEPER_WALL_HEAD, + Blocks.DRAGON_HEAD, + Blocks.DRAGON_WALL_HEAD, + Blocks.SKELETON_SKULL, + Blocks.SKELETON_WALL_SKULL, + Blocks.WITHER_SKELETON_SKULL, + Blocks.WITHER_SKELETON_WALL_SKULL, + Blocks.PIGLIN_HEAD, + Blocks.PIGLIN_WALL_HEAD, + Blocks.ZOMBIE_HEAD, + Blocks.ZOMBIE_WALL_HEAD, + Blocks.PLAYER_HEAD, + Blocks.PLAYER_WALL_HEAD, + Blocks.REPEATER, + Blocks.COMPARATOR, + Blocks.BIG_DRIPLEAF_STEM, + Blocks.MOSS_CARPET, + Blocks.PALE_MOSS_CARPET, + Blocks.COCOA, + Blocks.LADDER, + Blocks.SEA_PICKLE, + ), + setOf( + BlockTags.FLOWER_POTS, + BlockTags.WOOL_CARPETS, + ), + ) + val etherwarpConsidersFat = Checker( + setOf(), setOf( + // Wall signs have a hitbox + BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS, + BlockTags.BANNERS, + ) + ) + + + fun <T> checkTags(holder: Holder<out T>, set: Set<TagKey<out T>>) = + holder.tags() + .anyMatch(set::contains) + + + fun isEtherwarpTransparent(world: BlockGetter, blockPos: BlockPos): Boolean { + val blockState = world.getBlockState(blockPos) + val block = blockState.block + if (etherwarpConsidersFat.matches(blockState.blockHolder)) + return false + if (block.defaultBlockState().getCollisionShape(world, blockPos).isEmpty) + return true + if (etherwarpHallpasses.matches(blockState.blockHolder)) + return true + return false + } + + sealed interface EtherwarpBlockHit { + data class BlockHit(val blockPos: BlockPos, val accuratePos: Vec3?) : EtherwarpBlockHit + data object Miss : EtherwarpBlockHit + } + + fun raycastWithEtherwarpTransparency(world: BlockGetter, start: Vec3, end: Vec3): EtherwarpBlockHit { + return BlockGetter.traverseBlocks<EtherwarpBlockHit, Unit>( + start, end, Unit, + { _, blockPos -> + if (isEtherwarpTransparent(world, blockPos)) { + return@traverseBlocks null + } +// val defaultedState = world.getBlockState(blockPos).block.defaultState +// val hitShape = defaultedState.getCollisionShape( +// world, +// blockPos, +// ShapeContext.absent() +// ) +// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) { +// return@raycast null +// } + val partialResult = world.clipWithInteractionOverride(start, end, blockPos, Shapes.block(), world.getBlockState(blockPos).block.defaultBlockState()) + return@traverseBlocks EtherwarpBlockHit.BlockHit(blockPos, partialResult?.location) + }, + { EtherwarpBlockHit.Miss }) + } + + enum class EtherwarpItemKind { + MERGED, + RAW + } + + @Subscribe + fun renderEtherwarpOverlay(event: WorldRenderLastEvent) { + if (!TConfig.etherwarpOverlay) return + val player = MC.player ?: return + if (TConfig.onlyShowWhileSneaking && !player.isShiftKeyDown) return + val world = player.level + val heldItem = MC.stackInHand + val etherwarpTyp = run { + if (heldItem.extraAttributes.contains("ethermerge")) + EtherwarpItemKind.MERGED + else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT) + EtherwarpItemKind.RAW + else + return + } + val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21) + if (player.isShiftKeyDown || etherwarpTyp == EtherwarpItemKind.MERGED) + (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54) + else 1.62 + val playerEyePos = player.position.add(0.0, playerEyeHeight, 0.0) + val start = playerEyePos + val end = player.getViewVector(0F).scale(160.0).add(playerEyePos) + val hitResult = raycastWithEtherwarpTransparency( + world, + start, + end, + ) + if (hitResult !is EtherwarpBlockHit.BlockHit) return + val blockPos = hitResult.blockPos + val success = run { + if (!isEtherwarpTransparent(world, blockPos.above())) + EtherwarpResult.OCCUPIED + else if (!isEtherwarpTransparent(world, blockPos.above(2))) + EtherwarpResult.OCCUPIED + else if (playerEyePos.distanceToSqr(hitResult.accuratePos ?: blockPos.center) > 61 * 61) + EtherwarpResult.TOO_DISTANT + else if ((MC.instance.hitResult as? BlockHitResult) + ?.takeIf { it.type == HitResult.Type.BLOCK } + ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).blockHolder) } + ?: false + ) + EtherwarpResult.INTERACTION_BLOCKED + else + EtherwarpResult.SUCCESS + } + RenderInWorldContext.renderInWorld(event) { + if (TConfig.cube) + block( + blockPos, + success.color().getEffectiveColourRGB() + ) + if (TConfig.wireframe) wireframeCube(blockPos, 10f) + if (TConfig.failureText && success.label != null) { + withFacingThePlayer(blockPos.center) { + text(success.label) + } + } + } + } +} diff --git a/src/main/kotlin/features/items/recipes/ArrowWidget.kt b/src/main/kotlin/features/items/recipes/ArrowWidget.kt new file mode 100644 index 0000000..db0cf60 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ArrowWidget.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.resources.ResourceLocation + +class ArrowWidget(override var position: Point) : RecipeWidget() { + override val size: Dimension + get() = Dimension(14, 14) + + companion object { + val arrowSprite = ResourceLocation.withDefaultNamespace("container/furnace/lit_progress") + } + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + arrowSprite, + 14, + 14, + 0, + 0, + position.x, + position.y, + 14, + 14 + ) + } + +} diff --git a/src/main/kotlin/features/items/recipes/ComponentWidget.kt b/src/main/kotlin/features/items/recipes/ComponentWidget.kt new file mode 100644 index 0000000..08a2aa2 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ComponentWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.MC + +class ComponentWidget(override var position: Point, var text: Component) : RecipeWidget(), RecipeLayouter.Updater<Component> { + override fun update(newValue: Component) { + this.text = newValue + } + + override val size: Dimension + get() = Dimension(MC.font.width(text), MC.font.lineHeight) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawString(MC.font, text, position.x, position.y, -1) + } +} diff --git a/src/main/kotlin/features/items/recipes/EntityWidget.kt b/src/main/kotlin/features/items/recipes/EntityWidget.kt new file mode 100644 index 0000000..4a087e5 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/EntityWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.gui.entity.EntityRenderer + +class EntityWidget( + override var position: Point, + override val size: Dimension, + val entity: LivingEntity +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + EntityRenderer.renderEntity( + entity, guiGraphics, + rect.x, rect.y, + rect.width.toDouble(), rect.height.toDouble(), + mouseX.toDouble(), mouseY.toDouble() + ) + } +} diff --git a/src/main/kotlin/features/items/recipes/FireWidget.kt b/src/main/kotlin/features/items/recipes/FireWidget.kt new file mode 100644 index 0000000..565152b --- /dev/null +++ b/src/main/kotlin/features/items/recipes/FireWidget.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics + +class FireWidget(override var position: Point, val animationTicks: Int) : RecipeWidget() { + override val size: Dimension + get() = Dimension(10, 10) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/features/items/recipes/ItemList.kt b/src/main/kotlin/features/items/recipes/ItemList.kt new file mode 100644 index 0000000..340e1c3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemList.kt @@ -0,0 +1,308 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import java.util.Optional +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.navigation.ScreenAxis +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.input.MouseButtonInfo +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.api.v1.FirmamentAPI +import moe.nea.firmament.events.HandledScreenClickEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.accessors.castAccessor +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyblockId + +object ItemList { + // TODO: add a global toggle for this and RecipeRegistry + + fun collectExclusions(screen: Screen): Set<ScreenRectangle> { + val exclusions = mutableSetOf<ScreenRectangle>() + if (screen is AbstractContainerScreen<*>) { + val screenHandler = screen.castAccessor() + exclusions.add( + ScreenRectangle( + screenHandler.x_Firmament, + screenHandler.y_Firmament, + screenHandler.backgroundWidth_Firmament, + screenHandler.backgroundHeight_Firmament + ) + ) + } + FirmamentAPI.getInstance().extensions + .forEach { extension -> + for (rectangle in extension.getExclusionZones(screen)) { + if (exclusions.any { it.encompasses(rectangle) }) + continue + exclusions.add(rectangle) + } + } + + return exclusions + } + + var reachableItems = listOf<SBItemStack>() + var pageOffset = 0 + fun recalculateVisibleItems() { + reachableItems = RepoManager.neuRepo.items + .items.values.map { SBItemStack(it.skyblockId) } + } + + @Subscribe + fun onReload(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { recalculateVisibleItems() } + } + + fun coordinates(outer: ScreenRectangle, exclusions: Collection<ScreenRectangle>): Sequence<ScreenRectangle> { + val entryWidth = 18 + val columns = outer.width / entryWidth + val rows = outer.height / entryWidth + val lowX = outer.right() - columns * entryWidth + val lowY = outer.top() + return generateSequence(0) { it + 1 } + .map { + val xIndex = it % columns + val yIndex = it / columns + ScreenRectangle( + lowX + xIndex * entryWidth, lowY + yIndex * entryWidth, + entryWidth, entryWidth + ) + } + .take(rows * columns) + .filter { candidate -> exclusions.none { it.intersects(candidate) } } + } + + var lastRenderPositions: List<Pair<ScreenRectangle, SBItemStack>> = listOf() + var lastHoveredItemStack: Pair<ScreenRectangle, SBItemStack>? = null + + abstract class ItemListElement( + ) : Renderable, GuiEventListener { + abstract val rectangle: Rectangle + override fun setFocused(focused: Boolean) { + } + + override fun isFocused(): Boolean { + return false + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rectangle.contains(mouseX, mouseY) + } + } + + interface HasLabel { + fun component(): Component + } + + + class PopupSettingsElement<T : HasLabel>( + x: Int, + y: Int, + width: Int, + val selected: GetSetter<T>, + val options: List<T>, + ) : ItemListElement() { + override val rectangle: Rectangle = Rectangle(x, y, width, 4 + (MC.font.lineHeight + 2) * options.size) + fun bb(i: Int) = + Rectangle( + rectangle.minX, rectangle.minY + (2 + MC.font.lineHeight) * i + 2, + rectangle.width, MC.font.lineHeight + ) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.fill(rectangle.minX, rectangle.minY, rectangle.maxX, rectangle.maxY, 0xFF000000.toInt()) + guiGraphics.submitOutline(rectangle.x, rectangle.y, rectangle.width, rectangle.height, -1) + val sel = selected.get() + for ((index, element) in options.withIndex()) { + val b = bb(index) + val tw = MC.font.width(element.component()) + guiGraphics.drawString( + MC.font, element.component(), b.centerX - tw / 2, + b.y + 1, + if (element == sel) 0xFFA0B000.toInt() else -1 + ) + if (b.contains(mouseX, mouseY)) + guiGraphics.hLine(b.centerX - tw / 2, b.centerX + tw / 2 - 1, b.maxY + 1, -1) + } + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + popupElement = null + for ((index, element) in options.withIndex()) { + val b = bb(index) + if (b.contains(event.x, event.y)) { + selected.set(element) + break + } + } + return true + } + } + + class SettingElement<T : HasLabel>( + x: Int, + y: Int, + val selected: GetSetter<T>, + val options: List<T> + ) : ItemListElement() { + val height = MC.font.lineHeight + 4 + val width = options.maxOf { MC.font.width(it.component()) } + 4 + override val rectangle: Rectangle = Rectangle(x, y, width, height) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawCenteredString(MC.font, selected.get().component(), rectangle.centerX, rectangle.y + 2, -1) + if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) { + guiGraphics.hLine(rectangle.minX, rectangle.maxX - 1, rectangle.maxY - 2, -1) + } + } + + override fun mouseClicked( + event: MouseButtonEvent, + isDoubleClick: Boolean + ): Boolean { + popupElement = PopupSettingsElement( + rectangle.x, + rectangle.y - options.size * (MC.font.lineHeight + 2) - 2, + width, + selected, + options + ) + return true + } + } + + var popupElement: ItemListElement? = null + + + fun findStackUnder(mouseX: Int, mouseY: Int): Pair<ScreenRectangle, SBItemStack>? { + val lhis = lastHoveredItemStack + if (lhis != null && lhis.first.containsPoint(mouseX, mouseY)) + return lhis + return lastRenderPositions.firstOrNull { it.first.containsPoint(mouseX, mouseY) } + } + + val isItemListEnabled get() = false + + @Subscribe + fun onClick(event: HandledScreenClickEvent) { + if(!isItemListEnabled)return + val pe = popupElement + val me = MouseButtonEvent( + event.mouseX, event.mouseY, + MouseButtonInfo(event.button, 0) // TODO: missing modifiers + ) + if (pe != null) { + event.cancel() + if (!pe.isMouseOver(event.mouseX, event.mouseY)) { + popupElement = null + return + } + pe.mouseClicked( + me, + false + ) + return + } + listElements.forEach { + if (it.isMouseOver(event.mouseX, event.mouseY)) + it.mouseClicked(me, false) + } + } + + var listElements = listOf<ItemListElement>() + + @Subscribe + fun onRender(event: HandledScreenForegroundEvent) { + if(!isItemListEnabled) return + lastHoveredItemStack = null + lastRenderPositions = listOf() + val exclusions = collectExclusions(event.screen) + val potentiallyVisible = reachableItems.subList(pageOffset, reachableItems.size) + val screenWidth = event.screen.width + val rightThird = ScreenRectangle( + screenWidth - screenWidth / 3, 0, + screenWidth / 3, event.screen.height - MC.font.lineHeight - 4 + ) + val coords = coordinates(rightThird, exclusions) + + lastRenderPositions = coords.zip(potentiallyVisible.asSequence()).toList() + val isPopupHovered = popupElement?.isMouseOver(event.mouseX.toDouble(),event.mouseY.toDouble()) + ?: false + lastRenderPositions.forEach { (pos, stack) -> + val realStack = stack.asLazyImmutableItemStack() + val toRender = realStack ?: ItemStack(Items.PAINTING) + event.context.renderItem(toRender, pos.left() + 1, pos.top() + 1) + if (!isPopupHovered && pos.containsPoint(event.mouseX, event.mouseY)) { + lastHoveredItemStack = pos to stack + event.context.setTooltipForNextFrame( + MC.font, + if (realStack != null) + ItemSlotWidget.getTooltip(realStack) + else + stack.estimateLore(), + Optional.empty(), + event.mouseX, event.mouseY + ) + } + } + event.context.fill( + rightThird.left(), + rightThird.bottom(), + rightThird.right(), + event.screen.height, + 0xFF000000.toInt() + ) + val le = mutableListOf<ItemListElement>() + le.add( + SettingElement( + 0, + rightThird.bottom(), + sortOrder, + SortOrder.entries + ) + ) + val bottomWidth = le.sumOf { it.rectangle.width + 2 } - 2 + var startX = rightThird.getCenterInAxis(ScreenAxis.HORIZONTAL) - bottomWidth / 2 + le.forEach { + it.rectangle.translate(startX, 0) + startX += it.rectangle.width + 2 + } + le.forEach { it.render(event.context, event.mouseX, event.mouseY, event.delta) } + listElements = le + popupElement?.render(event.context, event.mouseX, event.mouseY, event.delta) + } + + enum class SortOrder(val component: Component) : HasLabel { + NAME(Component.literal("Name")), + RARITY(Component.literal("Rarity")); + + override fun component(): Component = component + } + + val sortOrder = Property.of(SortOrder.NAME) +} diff --git a/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt new file mode 100644 index 0000000..b659643 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt @@ -0,0 +1,141 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.TooltipFlag +import moe.nea.firmament.api.v1.FirmamentItemWidget +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters.shortFormat +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt + +class ItemSlotWidget( + point: Point, + var content: List<SBItemStack>, + val slotKind: RecipeLayouter.SlotKind +) : RecipeWidget(), + RecipeLayouter.CyclingItemSlot, + FirmamentItemWidget { + override var position = point + override val size get() = Dimension(16, 16) + val itemRect get() = Rectangle(position, Dimension(16, 16)) + + val backgroundTopLeft + get() = + if (slotKind.isBig) Point(position.x - 4, position.y - 4) + else Point(position.x - 1, position.y - 1) + val backgroundSize = + if (slotKind.isBig) Dimension(16 + 8, 16 + 8) + else Dimension(18, 18) + override val rect: Rectangle + get() = Rectangle(backgroundTopLeft, backgroundSize) + + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + val stack = current().asImmutableItemStack() + // TODO: draw slot background + if (stack.isEmpty) return + guiGraphics.renderItem(stack, position.x, position.y) + guiGraphics.renderItemDecorations( + MC.font, stack, position.x, position.y, + if (stack.count >= SHORT_NUM_CUTOFF) shortFormat(stack.count.toDouble()) + else null + ) + if (itemRect.contains(mouseX, mouseY) + && guiGraphics.containsPointInScissor(mouseX, mouseY) + ) guiGraphics.setTooltipForNextFrame( + MC.font, getTooltip(stack), Optional.empty(), + mouseX, mouseY + ) + } + + companion object { + val SHORT_NUM_CUTOFF = 1000 + var canUseTooltipEvent = true + + fun getTooltip(itemStack: ItemStack): List<Component> { + val lore = mutableListOf(itemStack.displayNameAccordingToNbt) + lore.addAll(itemStack.loreAccordingToNbt) + if (canUseTooltipEvent) { + try { + ItemTooltipCallback.EVENT.invoker().getTooltip( + itemStack, Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, lore + ) + } catch (ex: Exception) { + canUseTooltipEvent = false + ErrorUtil.softError("Failed to use vanilla tooltips", ex) + } + } else { + ItemTooltipEvent.publish( + ItemTooltipEvent( + itemStack, + Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, + lore + ) + ) + } + if (itemStack.count >= SHORT_NUM_CUTOFF && lore.isNotEmpty()) + lore.add(1, Component.literal("${itemStack.count}x").darkGrey()) + return lore + } + } + + + override fun tick() { + if (SavedKeyBinding.isShiftDown()) return + if (content.size <= 1) return + if (MC.currentTick % 5 != 0) return + index = (index + 1) % content.size + } + + var index = 0 + var onUpdate: () -> Unit = {} + + override fun onUpdate(action: () -> Unit) { + this.onUpdate = action + } + + override fun current(): SBItemStack { + return content.getOrElse(index) { SBItemStack.EMPTY } + } + + override fun update(newValue: SBItemStack) { + content = listOf(newValue) + // SAFE: content was just assigned to a non-empty list + index = index.coerceIn(content.indices) + } + + override fun getPlacement(): FirmamentItemWidget.Placement { + return FirmamentItemWidget.Placement.RECIPE_SCREEN + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun getItemStack(): ItemStack { + return current().asImmutableItemStack() + } + + override fun getSkyBlockId(): String { + return current().skyblockId.neuItem + } +} diff --git a/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt new file mode 100644 index 0000000..aad3bda --- /dev/null +++ b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.input.MouseButtonEvent +import moe.nea.firmament.util.MoulConfigUtils.createAndTranslateFullContext + +class MoulConfigWidget( + val component: GuiComponent, + override var position: Point, + override val size: Dimension, +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + createAndTranslateFullContext( + guiGraphics, mouseX, mouseY, rect, + component::render + ) + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + return createAndTranslateFullContext(null, event.x.toInt(), event.y.toInt(), rect) { + component.mouseEvent(MouseEvent.Click(event.button(), true), it) + } + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + createAndTranslateFullContext(null, mouseX, mouseY, rect) { + component.mouseEvent(MouseEvent.Move(0F, 0F), it) + } + } + + override fun mouseReleased(event: MouseButtonEvent): Boolean { + return createAndTranslateFullContext(null, event.x, event.y, rect) { + component.mouseEvent(MouseEvent.Click(event.button(), false), it) + } + } + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeRegistry.kt b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt new file mode 100644 index 0000000..c2df46f --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt @@ -0,0 +1,116 @@ +package moe.nea.firmament.features.items.recipes + +import com.mojang.blaze3d.platform.InputConstants +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer +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.repo.recipes.SBReforgeRecipeRenderer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack + +object RecipeRegistry { + val recipeTypes: List<GenericRecipeRenderer<*>> = listOf( + SBCraftingRecipeRenderer, + SBForgeRecipeRenderer, + SBReforgeRecipeRenderer, + SBEssenceUpgradeRecipeRenderer, + ) + + + @Subscribe + fun showUsages(event: HandledScreenKeyPressedEvent) { + val provider = + if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_R))) { + ::getRecipesFor + } else if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_U))) { + ::getUsagesFor + } else { + return + } + val stack = event.screen.focusedItemStack ?: return + val recipes = provider(SBItemStack(stack)) + if (recipes.isEmpty()) return + MC.screen = RecipeScreen(recipes.toList()) + } + + + object RecipeIndexes : IReloadable { + + private fun <T : Any> createIndexFor( + neuRepository: NEURepository, + recipeRenderer: GenericRecipeRenderer<T>, + outputs: Boolean, + ): List<Pair<SkyblockId, RenderableRecipe<T>>> { + val indexer: (T) -> Collection<SBItemStack> = + if (outputs) recipeRenderer::getOutputs + else recipeRenderer::getInputs + return recipeRenderer.findAllRecipes(neuRepository) + .flatMap { + val wrappedRecipe = RenderableRecipe(it, recipeRenderer, null) + indexer(it).map { it.skyblockId to wrappedRecipe } + } + } + + fun createIndex(outputs: Boolean): MutableMap<SkyblockId, List<RenderableRecipe<*>>> { + val m: MutableMap<SkyblockId, List<RenderableRecipe<*>>> = mutableMapOf() + recipeTypes.forEach { renderer -> + createIndexFor(RepoManager.neuRepo, renderer, outputs) + .forEach { (stack, recipe) -> + m.merge(stack, listOf(recipe)) { a, b -> a + b } + } + } + return m + } + + lateinit var recipesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + lateinit var usagesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + override fun reload(recipe: NEURepository) { + recipesForIndex = createIndex(true) + usagesForIndex = createIndex(false) + } + } + + @Subscribe + fun onRepoBuild(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener(RecipeIndexes) + } + + + fun getRecipesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, true) } + recipes.addAll(RecipeIndexes.recipesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + fun getUsagesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, false) } + recipes.addAll(RecipeIndexes.usagesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + private fun <T : Any> injectRecipesFor( + recipeRenderer: GenericRecipeRenderer<T>, + collector: MutableCollection<RenderableRecipe<*>>, + relevantItem: SBItemStack, + mustBeInOutputs: Boolean + ) { + collector.addAll( + recipeRenderer.discoverExtraRecipes(RepoManager.neuRepo, relevantItem, mustBeInOutputs) + .map { RenderableRecipe(it, recipeRenderer, relevantItem) } + ) + } + + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeScreen.kt b/src/main/kotlin/features/items/recipes/RecipeScreen.kt new file mode 100644 index 0000000..9a746f3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeScreen.kt @@ -0,0 +1,129 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.renderer.RenderPipelines +import moe.nea.firmament.util.mc.CommonTextures +import moe.nea.firmament.util.render.enableScissorWithTranslation +import moe.nea.firmament.util.tr + +class RecipeScreen( + val recipes: List<RenderableRecipe<*>>, +) : Screen(tr("firmament.recipe.screen", "SkyBlock Recipe")) { + + data class PlacedRecipe( + val bounds: Rectangle, + val layoutedRecipe: StandaloneRecipeRenderer, + ) { + fun moveTo(position: Point) { + val Δx = position.x - bounds.x + val Δy = position.y - bounds.y + bounds.translate(Δx, Δy) + layoutedRecipe.widgets.forEach { widget -> + widget.position = widget.position.clone().also { + it.translate(Δx, Δy) + } + } + } + } + + lateinit var placedRecipes: List<PlacedRecipe> + var scrollViewport: Int = 0 + var scrollOffset: Int = 0 + var scrollPortWidth: Int = 0 + var heightEstimate: Int = 0 + val gutter = 10 + override fun init() { + super.init() + scrollViewport = minOf(height - 20, 250) + scrollPortWidth = 0 + heightEstimate = 0 + var offset = height / 2 - scrollViewport / 2 + placedRecipes = recipes.map { + val effectiveWidth = minOf(it.renderer.displayWidth, width - 20) + val bounds = Rectangle( + width / 2 - effectiveWidth / 2, + offset, + effectiveWidth, + it.renderer.displayHeight + ) + if (heightEstimate > 0) + heightEstimate += gutter + heightEstimate += bounds.height + scrollPortWidth = maxOf(effectiveWidth, scrollPortWidth) + offset += bounds.height + gutter + val layoutedRecipe = it.render(bounds) + layoutedRecipe.widgets.forEach(this::addRenderableWidget) + PlacedRecipe(bounds, layoutedRecipe) + } + } + + fun scrollRect() = + Rectangle( + width / 2 - scrollPortWidth / 2, height / 2 - scrollViewport / 2, + scrollPortWidth, scrollViewport + ) + + fun scissorScrollPort(guiGraphics: GuiGraphics) { + guiGraphics.enableScissorWithTranslation(scrollRect()) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { + if (!scrollRect().contains(mouseX, mouseY)) + return false + scrollOffset = (scrollOffset + scrollY * -4) + .coerceAtMost(heightEstimate - scrollViewport.toDouble()) + .coerceAtLeast(.0) + .toInt() + var offset = height / 2 - scrollViewport / 2 - scrollOffset + placedRecipes.forEach { + it.moveTo(Point(it.bounds.x, offset)) + offset += it.bounds.height + gutter + } + return true + } + + override fun renderBackground( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + super.renderBackground(guiGraphics, mouseX, mouseY, partialTick) + + val srect = scrollRect() + srect.grow(8, 8) + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + srect.x, srect.y, + srect.width, srect.height + ) + + scissorScrollPort(guiGraphics) + placedRecipes.forEach { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + it.bounds.x, it.bounds.y, + it.bounds.width, it.bounds.height + ) + } + guiGraphics.disableScissor() + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + scissorScrollPort(guiGraphics) + super.render(guiGraphics, mouseX, mouseY, partialTick) + guiGraphics.disableScissor() + } + + override fun tick() { + super.tick() + placedRecipes.forEach { + it.layoutedRecipe.tick() + } + } +} diff --git a/src/main/kotlin/features/items/recipes/RecipeWidget.kt b/src/main/kotlin/features/items/recipes/RecipeWidget.kt new file mode 100644 index 0000000..f13707c --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeWidget.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.narration.NarratableEntry +import net.minecraft.client.gui.narration.NarrationElementOutput +import net.minecraft.client.gui.navigation.ScreenRectangle +import moe.nea.firmament.util.mc.asScreenRectangle + +abstract class RecipeWidget : GuiEventListener, Renderable, NarratableEntry { + override fun narrationPriority(): NarratableEntry.NarrationPriority? { + return NarratableEntry.NarrationPriority.NONE// I am so sorry + } + + override fun updateNarration(narrationElementOutput: NarrationElementOutput) { + } + + open fun tick() {} + private var _focused = false + abstract var position: Point + abstract val size: Dimension + open val rect: Rectangle get() = Rectangle(position, size) + override fun setFocused(focused: Boolean) { + this._focused = focused + } + + override fun isFocused(): Boolean { + return this._focused + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rect.contains(mouseX, mouseY) + } +} diff --git a/src/main/kotlin/features/items/recipes/RenderableRecipe.kt b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt new file mode 100644 index 0000000..20ca17e --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Objects +import me.shedaniel.math.Rectangle +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer + +class RenderableRecipe<T : Any>( + val recipe: T, + val renderer: GenericRecipeRenderer<T>, + val mainItemStack: SBItemStack?, +) { + fun render(bounds: Rectangle): StandaloneRecipeRenderer { + val layouter = StandaloneRecipeRenderer(bounds) + renderer.render(recipe, bounds, layouter, mainItemStack) + return layouter + } + +// override fun equals(other: Any?): Boolean { +// if (other !is RenderableRecipe<*>) return false +// return renderer == other.renderer && recipe == other.recipe +// } +// +// override fun hashCode(): Int { +// return Objects.hash(recipe, renderer) +// } +} diff --git a/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt new file mode 100644 index 0000000..5a834eb --- /dev/null +++ b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class StandaloneRecipeRenderer(val bounds: Rectangle) : AbstractContainerEventHandler(), RecipeLayouter { + + fun tick() { + widgets.forEach { it.tick() } + } + + fun <T : RecipeWidget> addWidget(widget: T): T { + this.widgets.add(widget) + return widget + } + + override fun createCyclingItemSlot( + x: Int, + y: Int, + content: List<SBItemStack>, + slotKind: RecipeLayouter.SlotKind + ): RecipeLayouter.CyclingItemSlot { + return addWidget(ItemSlotWidget(Point(x, y), content, slotKind)) + } + + val Rectangle.topLeft get() = Point(x, y) + + override fun createTooltip( + rectangle: Rectangle, + label: List<Component> + ) { + addWidget(TooltipWidget(rectangle.topLeft, rectangle.size, label)) + } + + override fun createLabel( + x: Int, + y: Int, + text: Component + ): RecipeLayouter.Updater<Component> { + return addWidget(ComponentWidget(Point(x, y), text)) + } + + override fun createArrow(x: Int, y: Int): Rectangle { + return addWidget(ArrowWidget(Point(x, y))).rect + } + + override fun createMoulConfig( + x: Int, + y: Int, + w: Int, + h: Int, + component: GuiComponent + ) { + addWidget(MoulConfigWidget(component, Point(x, y), Dimension(w, h))) + } + + override fun createFire(point: Point, animationTicks: Int) { + addWidget(FireWidget(point, animationTicks)) + } + + override fun createEntity(rectangle: Rectangle, entity: LivingEntity) { + addWidget(EntityWidget(rectangle.topLeft, rectangle.size, entity)) + } + + val widgets: MutableList<RecipeWidget> = mutableListOf() + override fun children(): List<GuiEventListener> { + return widgets + } +} diff --git a/src/main/kotlin/features/items/recipes/TooltipWidget.kt b/src/main/kotlin/features/items/recipes/TooltipWidget.kt new file mode 100644 index 0000000..87feb61 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/TooltipWidget.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class TooltipWidget( + override var position: Point, + override val size: Dimension, + label: List<Component> +) : RecipeWidget(), RecipeLayouter.Updater<List<Component>> { + override fun update(newValue: List<Component>) { + this.formattedComponent = newValue.map { it.visualOrderText } + } + + var formattedComponent = label.map { it.visualOrderText } + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + if (rect.contains(mouseX, mouseY)) + guiGraphics.setTooltipForNextFrame(formattedComponent, mouseX, mouseY) + } + +} diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt new file mode 100644 index 0000000..9dadb80 --- /dev/null +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.features.macros + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.network.chat.Component +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<SavedKeyBinding>() + + fun setActions(actions: List<ComboKeyAction>) { + 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.pose().pushMatrix() + val width = 120 + event.context.pose().translate( + (MC.window.guiScaledWidth - width) / 2F, + (MC.window.guiScaledHeight) / 2F + 8 + ) + val breadCrumbText = breadCrumbs.joinToString(" > ") + event.context.drawString( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 2F) + for ((key, value) in activeTrie.nodes) { + event.context.drawString( + MC.font, + Component.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 1F) + } + event.context.pose().popMatrix() + } + + @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..18c95bc --- /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.network.chat.Component +import moe.nea.firmament.util.MC + +@Serializable +sealed interface HotkeyAction { + // TODO: execute + val label: Component + fun execute() +} + +@Serializable +@SerialName("command") +data class CommandAction(val command: String) : HotkeyAction { + override val label: Component + get() = Component.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..2701ac1 --- /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.network.chat.Component +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil + +sealed interface KeyComboTrie { + val label: Component + + companion object { + fun fromComboList( + combos: List<ComboKeyAction>, + ): Branch { + val root = Branch(mutableMapOf()) + for (combo in combos) { + var p = root + if (combo.keySequence.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keySequence.withIndex()) { + val m = (p.nodes as MutableMap) + if (index == combo.keySequence.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence.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.keySequence} (final node exists at index $index) through another action already") + break + } else { + p = c + } + } + } + } + return root + } + } +} + +@Serializable +data class MacroWheel( + val keyBinding: SavedKeyBinding = SavedKeyBinding.unbound(), + val options: List<HotkeyAction> +) + +@Serializable +data class ComboKeyAction( + val action: HotkeyAction, + val keySequence: List<SavedKeyBinding> = listOf(), +) + +data class Leaf(val action: HotkeyAction) : KeyComboTrie { + override val label: Component + get() = action.label + + fun execute() { + action.execute() + } +} + +data class Branch( + val nodes: Map<SavedKeyBinding, KeyComboTrie> +) : KeyComboTrie { + override val label: Component + get() = Component.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..af1b0e8 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List<ComboKeyAction> = listOf(), + var wheels: List<MacroWheel> = listOf(), +) { + @Config + object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) { + override fun onLoad() { + ComboProcessor.setActions(data.comboActions) + RadialMacros.setWheels(data.wheels) + } + } +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..e73f076 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,293 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.common.text.StructuredText +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +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?.onClose() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @Bind + fun textR() = StructuredText.of(text) + + @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<CommandAction>, + ) { + + fun asSaveable(): MacroWheel { + return MacroWheel(binding, commands.map { it.asCommandAction() }) + } + + @Bind("keyCombo") + fun text() = MoulConfigPlatform.wrap(binding.format()) + + @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?.onClose() + } + + @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<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) { + Wheel(this, it.keyBinding, 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?.onClose() + } + + inner class Combos { + @field:Bind("actions") + val actions: ObservableList<ActionEditor> = 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 + + @Bind + fun commandR() = StructuredText.of(command) + + @field:Bind("combo") + val combo = action.keySequence.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + StructuredText.of(combo.joinToString(" > ") { it.binding.toString() }) // TODO: this can be joined without .toString() + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.onClose() + } + + @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 <T> ObservableList<T>.setAll(ts: Collection<T>) { + 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..2519123 --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,153 @@ +package moe.nea.firmament.features.macros + +import me.shedaniel.math.Color +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.GuiGraphics +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<RadialMenuOption> + } + + interface RadialMenuOption { + val isEnabled: Boolean + fun resolve() + fun renderSlice(drawContext: GuiGraphics) + } + + 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.pose() + mat.pushMatrix() + mat.translate( + (MC.window.guiScaledWidth) / 2F, + (MC.window.guiScaledHeight) / 2F, + ) + 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.pushMatrix() + mat.scale(64F, 64F) + 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.popMatrix() + mat.pushMatrix() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y) + option.renderSlice(event.context) + mat.popMatrix() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), Color.ofOpaque(0x00FF00)) + mat.popMatrix() + } + + @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 { + lateinit var wheels: List<MacroWheel> + private set + + fun setWheels(wheels: List<MacroWheel>) { + this.wheels = wheels + RadialMenuViewer.activeMenu = null + } + + @Subscribe + fun onOpen(event: WorldKeyboardEvent) { + if (RadialMenuViewer.activeMenu != null) return + wheels.forEach { wheel -> + if (event.matches(wheel.keyBinding, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: GuiGraphics) { + drawContext.drawCenteredString(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.keyBinding + override val options: List<RadialMenuOption> = + wheel.options.map { R(it) } + } + } + } + } +} diff --git a/src/main/kotlin/features/mining/CommissionFeatures.kt b/src/main/kotlin/features/mining/CommissionFeatures.kt index faba253..bfc635a 100644 --- a/src/main/kotlin/features/mining/CommissionFeatures.kt +++ b/src/main/kotlin/features/mining/CommissionFeatures.kt @@ -3,22 +3,24 @@ 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.unformattedString object CommissionFeatures { - object Config : ManagedConfig("commissions", Category.MINING) { + @Config + object TConfig : ManagedConfig("commissions", Category.MINING) { val highlightCompletedCommissions by toggle("highlight-completed") { true } } @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { - if (!Config.highlightCompletedCommissions) return + if (!TConfig.highlightCompletedCommissions) return if (MC.screenName != "Commissions") return - val stack = event.slot.stack + val stack = event.slot.item 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 index ed48437..08ee893 100644 --- a/src/main/kotlin/features/mining/Histogram.kt +++ b/src/main/kotlin/features/mining/Histogram.kt @@ -1,7 +1,8 @@ package moe.nea.firmament.features.mining -import java.util.* +import java.util.NavigableMap +import java.util.TreeMap import kotlin.time.Duration import moe.nea.firmament.util.TimeMark diff --git a/src/main/kotlin/features/mining/HotmPresets.kt b/src/main/kotlin/features/mining/HotmPresets.kt index 2241fee..4930f90 100644 --- a/src/main/kotlin/features/mining/HotmPresets.kt +++ b/src/main/kotlin/features/mining/HotmPresets.kt @@ -3,14 +3,15 @@ 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 net.minecraft.world.level.block.Blocks +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Items +import net.minecraft.world.inventory.ChestMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute @@ -18,12 +19,12 @@ 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.accessors.castAccessor import moe.nea.firmament.util.customgui.CustomGui import moe.nea.firmament.util.customgui.customGui import moe.nea.firmament.util.mc.CommonTextures @@ -51,8 +52,8 @@ object HotmPresets { 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 + val screen = event.new as? AbstractContainerScreen<*> ?: return + val oldHandler = (event.old as? AbstractContainerScreen<*>)?.customGui if (oldHandler is HotmScrollPrompt) { event.new.customGui = oldHandler oldHandler.setNewScreen(screen) @@ -63,32 +64,32 @@ object HotmPresets { screen.customGui = HotmScrollPrompt(screen) } - class HotmScrollPrompt(var screen: HandledScreen<*>) : CustomGui() { + class HotmScrollPrompt(var screen: AbstractContainerScreen<*>) : CustomGui() { var bounds = Rectangle( 0, 0, 0, 0 ) - fun setNewScreen(screen: HandledScreen<*>) { + fun setNewScreen(screen: AbstractContainerScreen<*>) { this.screen = screen onInit() hasScrolled = false } - override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + override fun render(drawContext: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) { drawContext.drawGuiTexture( CommonTextures.genericWidget(), bounds.x, bounds.y, bounds.width, bounds.height, ) - drawContext.drawCenteredTextWithShadow( + drawContext.drawCenteredString( MC.font, if (hasAll) { - Text.translatable("firmament.hotmpreset.copied") + Component.translatable("firmament.hotmpreset.copied") } else if (!hasScrolled) { - Text.translatable("firmament.hotmpreset.scrollprompt") + Component.translatable("firmament.hotmpreset.scrollprompt") } else { - Text.translatable("firmament.hotmpreset.scrolled") + Component.translatable("firmament.hotmpreset.scrolled") }, bounds.centerX, bounds.centerY - 5, @@ -100,14 +101,14 @@ object HotmPresets { var hasScrolled = false var hasAll = false - override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { if (!hasScrolled) { - val slot = screen.screenHandler.getSlot(8) - println("Clicking ${slot.stack}") - slot.clickRightMouseButton(screen.screenHandler) + val slot = screen.menu.getSlot(8) + println("Clicking ${slot.item}") + slot.clickRightMouseButton(screen.menu) } hasScrolled = true - return super.mouseClick(mouseX, mouseY, button) + return super.mouseClick(click, doubled) } override fun shouldDrawForeground(): Boolean { @@ -124,7 +125,7 @@ object HotmPresets { screen.height / 2 - 100, 300, 200 ) - val screen = screen as AccessorHandledScreen + val screen = screen.castAccessor() screen.x_Firmament = bounds.x screen.y_Firmament = bounds.y screen.backgroundWidth_Firmament = bounds.width @@ -140,10 +141,10 @@ object HotmPresets { val allRows = (1..10).toSet() fun onNewItems(event: ChestInventoryUpdateEvent) { - val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return + val handler = screen.menu as? ChestMenu ?: return for (it in handler.slots) { - if (it.inventory is PlayerInventory) continue - val stack = it.stack + if (it.container is Inventory) continue + val stack = it.item val name = stack.displayNameAccordingToNbt.unformattedString tierRegex.useMatch(name) { coveredRows.add(group("tier").toInt()) @@ -156,7 +157,9 @@ object HotmPresets { } } if (allRows == coveredRows) { - ClipboardUtils.setTextContent(TemplateUtil.encodeTemplate(SHARE_PREFIX, HotmPreset( + ClipboardUtils.setTextContent( + TemplateUtil.encodeTemplate( + SHARE_PREFIX, HotmPreset( unlockedPerks.map { PerkPreset(it) } ))) hasAll = true @@ -169,7 +172,7 @@ object HotmPresets { @Subscribe fun onSlotUpdates(event: ChestInventoryUpdateEvent) { - val customGui = (event.inventory as? HandledScreen<*>)?.customGui + val customGui = (event.inventory as? AbstractContainerScreen<*>)?.customGui if (customGui is HotmScrollPrompt) { customGui.onNewItems(event) } @@ -185,7 +188,7 @@ object HotmPresets { @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { if (hotmInventoryName == MC.screenName - && event.slot.stack.displayNameAccordingToNbt.unformattedString in highlightedPerks + && event.slot.item.displayNameAccordingToNbt.unformattedString in highlightedPerks ) { event.highlight((Firmament.identifier("hotm_perk_preset"))) } @@ -197,7 +200,7 @@ object HotmPresets { thenExecute { hotmCommandSent = TimeMark.now() MC.sendCommand("hotm") - source.sendFeedback(Text.translatable("firmament.hotmpreset.openinghotm")) + source.sendFeedback(Component.translatable("firmament.hotmpreset.openinghotm")) } } event.subcommand("importhotm") { @@ -205,10 +208,10 @@ object HotmPresets { val template = TemplateUtil.maybeDecodeTemplate<HotmPreset>(SHARE_PREFIX, ClipboardUtils.getTextContents()) if (template == null) { - source.sendFeedback(Text.translatable("firmament.hotmpreset.failedimport")) + source.sendFeedback(Component.translatable("firmament.hotmpreset.failedimport")) } else { highlightedPerks = template.perks.mapTo(mutableSetOf()) { it.perkName } - source.sendFeedback(Text.translatable("firmament.hotmpreset.okayimport")) + source.sendFeedback(Component.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..6c50665 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,53 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screens.Screen +import net.minecraft.world.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 = MoulConfigPlatform.wrap(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 index 94b49f9..23b55e5 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -1,13 +1,17 @@ package moe.nea.firmament.features.mining +import io.github.notenoughupdates.moulconfig.ChromaColour import java.util.regex.Pattern +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -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 net.minecraft.client.Minecraft +import net.minecraft.client.gui.components.toasts.SystemToast +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.DyeColor +import net.minecraft.world.InteractionHand +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.ProcessChatEvent @@ -15,8 +19,6 @@ 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 @@ -24,6 +26,8 @@ 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -33,20 +37,40 @@ 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 +object PickaxeAbility { + val identifier: String get() = "pickaxe-info" + enum class ShowOnTools(val label: String, val items: Set<ItemType>) : StringRepresentable { + ALL("all", ItemType.DRILL, ItemType.PICKAXE, ItemType.SHOVEL, ItemType.AXE), + PICKAXES_AND_DRILLS("pick-and-drill", ItemType.PICKAXE, ItemType.DRILL), + DRILLS("drills", ItemType.DRILL), + ; + override fun getSerializedName(): String? { + return label + } + + constructor(label: String, vararg items: ItemType) : this(label, items.toSet()) + + fun matches(type: ItemType) = items.contains(type) + } + + @Config object TConfig : ManagedConfig(identifier, Category.MINING) { val cooldownEnabled by toggle("ability-cooldown") { false } + val disableInDungeons by toggle("disable-in-dungeons") { true } + val showOnTools by choice("show-on-tools") { ShowOnTools.PICKAXES_AND_DRILLS } val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val cooldownColour by colour("ability-colour") { ChromaColour.fromStaticRGB(187, 54, 44, 128) } + val cooldownReadyToast by toggle("ability-cooldown-toast") { false } val drillFuelBar by toggle("fuel-bar") { true } val blockOnPrivateIsland by choice( "block-on-dynamic", @@ -55,12 +79,12 @@ object PickaxeAbility : FirmamentFeature { } } - enum class BlockPickaxeAbility : StringIdentifiable { + enum class BlockPickaxeAbility : StringRepresentable { NEVER, ALWAYS, ONLY_DESTRUCTIVE; - override fun asString(): String { + override fun getSerializedName(): String { return name } } @@ -79,9 +103,6 @@ object PickaxeAbility : FirmamentFeature { 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() @@ -108,9 +129,12 @@ object PickaxeAbility : FirmamentFeature { 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") + 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() } @@ -140,9 +164,9 @@ object PickaxeAbility : FirmamentFeature { } } ?: return val extra = it.item.extraAttributes - if (!extra.contains("drill_fuel")) return - val fuel = extra.getInt("drill_fuel") - val percentage = fuel / maxFuel.toFloat() + 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(), @@ -156,10 +180,31 @@ object PickaxeAbility : FirmamentFeature { 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: Minecraft = Minecraft.getInstance() + mc.toastManager.addToast( + SystemToast.multiline( + mc, + SystemToast.SystemToastId.NARRATOR_TOGGLE, + tr("firmament.pickaxe.ability-ready", "Pickaxe Cooldown"), + tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!") + ) + ) + } } @Subscribe @@ -179,6 +224,7 @@ object PickaxeAbility : FirmamentFeature { val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)") val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.") + val nowAvailable = Pattern.compile("(?<name>[a-zA-Z0-9 ]+) is now available!") data class PickaxeAbilityData( val name: String, @@ -200,21 +246,27 @@ object PickaxeAbility : FirmamentFeature { @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 stack = MC.player?.getItemInHand(InteractionHand.MAIN_HAND) ?: return + if (!TConfig.showOnTools.matches(ItemType.fromItemStack(stack) ?: ItemType.NIL)) + return + var ability = getCooldownFromLore(stack)?.also { ability -> + defaultAbilityDurations[ability.name] = ability.cooldown + } val ao = abilityOverride - if (ao != ability.name && ao != null) { - ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds) + if (ability == null || (ao != ability.name && ao != null)) { + ability = PickaxeAbilityData(ao ?: return, 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) + event.context.pose().pushMatrix() + event.context.pose().translate(MC.window.guiScaledWidth / 2F, MC.window.guiScaledHeight / 2F) + event.context.pose().scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat()) RenderCircleProgress.renderCircle( - event.context, Identifier.of("firmament", "textures/gui/circle.png"), + event.context, ResourceLocation.fromNamespaceAndPath("firmament", "textures/gui/circle.png"), getCooldownPercentage(ability.name, ability.cooldown).toFloat(), - 0f, 1f, 0f, 1f + 0f, 1f, 0f, 1f, + color = TConfig.cooldownColour.getEffectiveColourRGB() ) - event.context.matrices.pop() + event.context.pose().popMatrix() } } diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt index 377a470..0470702 100644 --- a/src/main/kotlin/features/mining/PristineProfitTracker.kt +++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt @@ -1,26 +1,26 @@ package moe.nea.firmament.features.mining import io.github.notenoughupdates.moulconfig.xml.Bind -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import kotlinx.serialization.Serializable import kotlinx.serialization.serializer import kotlin.time.Duration.Companion.seconds -import net.minecraft.text.Text +import net.minecraft.network.chat.Component 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.Config +import moe.nea.firmament.util.data.ManagedConfig 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 +object PristineProfitTracker { + val identifier: String get() = "pristine-profit" enum class GemstoneKind( @@ -50,14 +50,13 @@ object PristineProfitTracker : FirmamentFeature { var maxCollectionPerSecond: Double = 1.0, ) + @Config object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data) - override val config: ManagedConfig? - get() = TConfig - + @Config 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 gui by position("position", 100, 30) { Vector2i() } val useFineGemstones by toggle("fine-gemstones") { false } } @@ -106,28 +105,26 @@ object PristineProfitTracker : FirmamentFeature { val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds) if (collectionPerSecond == null || moneyPerSecond == null) return ProfitHud.collectionCurrent = collectionPerSecond - ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection", + ProfitHud.collectionText = Component.translatableEscape("firmament.pristine-profit.collection", formatCommas(collectionPerSecond * SECONDS_PER_HOUR, 1)).formattedString() ProfitHud.moneyCurrent = moneyPerSecond - ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money", + ProfitHud.moneyText = Component.translatableEscape("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) + 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) } diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt new file mode 100644 index 0000000..fe54e6b --- /dev/null +++ b/src/main/kotlin/features/misc/CustomCapes.kt @@ -0,0 +1,172 @@ +package moe.nea.firmament.features.misc + +import util.render.CustomRenderPipelines +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.player.AbstractClientPlayer +import net.minecraft.client.renderer.RenderType +import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.client.renderer.entity.state.AvatarRenderState +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.world.entity.player.PlayerSkin +import net.minecraft.core.ClientAsset +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.mc.CustomRenderPassHelper + +object CustomCapes { + val identifier: String + get() = "developer-capes" + + @Config + object TConfig : ManagedConfig(identifier, Category.DEV) { + val showCapes by toggle("show-cape") { true } + } + + interface CustomCapeRenderer { + fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) + } + + data class TexturedCapeRenderer( + val location: ResourceLocation + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) { + model(vertexConsumerProvider.getBuffer(RenderType.entitySolid(location))) + } + } + + data class ParallaxedHighlightCapeRenderer( + val template: ResourceLocation, + val background: ResourceLocation, + val overlay: ResourceLocation, + val animationSpeed: Duration, + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) { + val animationValue = (startTime.passedTime() / animationSpeed).mod(1F) + CustomRenderPassHelper( + { "Firmament Cape Renderer" }, + renderLayer.mode(), + renderLayer.format(), + MC.instance.mainRenderTarget, + true, + ).use { renderPass -> + renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER) + renderPass.setAllDefaultUniforms() + renderPass.setUniform("Animation", 4) { + it.putFloat(animationValue.toFloat()) + } + renderPass.bindSampler("Sampler0", template) + renderPass.bindSampler("Sampler1", background) + renderPass.bindSampler("Sampler3", overlay) + renderPass.uploadVertices(2048, model) + renderPass.draw() + } + } + } + + interface CapeStorage { + companion object { + @JvmStatic + fun cast(playerEntityRenderState: AvatarRenderState) = + 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 + ) + ), + UNPLEASANT_GRADIENT( + "unpleasant_gradient", + TexturedCapeRenderer(Firmament.identifier("textures/cape/unpleasant_gradient.png")) + ), + FURFSKY_STATIC( + "FurfSky", + TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png")) + ), + + FIRMAMENT_STATIC( + "Firmament", + TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png")) + ), + HYPIXEL_PLUS( + "Hypixel+", + TexturedCapeRenderer(Firmament.identifier("textures/cape/h_plus.png")) + ), + ; + + val cape = CustomCape(name, label, render) + } + + val byId = AllCapes.entries.associateBy { it.cape.id } + val byUuid = + listOf( + listOf( + Devs.nea to AllCapes.UNPLEASANT_GRADIENT, + Devs.kath to AllCapes.FIRMAMENT_STATIC, + Devs.jani to AllCapes.FIRMAMENT_ANIMATED, + Devs.nat to AllCapes.FIRMAMENT_ANIMATED, + Devs.HPlus.ic22487 to AllCapes.HYPIXEL_PLUS, + ), + Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC }, + ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap() + + @JvmStatic + fun addCapeData( + player: AbstractClientPlayer, + playerEntityRenderState: AvatarRenderState + ) { + if (true) return // TODO: see capefeaturerenderer mixin + 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.skin = PlayerSkin( + playerEntityRenderState.skin.body, + ClientAsset.ResourceTexture(Firmament.identifier("placeholder/fake_cape"), Firmament.identifier("placeholder/fake_cape")), + playerEntityRenderState.skin.elytra, + playerEntityRenderState.skin.model, + playerEntityRenderState.skin.secure, + ) + playerEntityRenderState.showCape = 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..b9f7e03 --- /dev/null +++ b/src/main/kotlin/features/misc/Devs.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.features.misc + +import java.util.UUID + +object Devs { + data class Dev( + val uuids: List<UUID>, + ) { + 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") + val nat = Dev("168300e6-4e74-4a6d-89a0-7b7cf8ad6a7d", "06914e9d-7bc2-4cb7-b112-62c4cc958d96") + + 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 + ) + } + object HPlus { + val ic22487 = Dev("ab2be3b2-bb75-4aaa-892d-9fff5a7e3953") + } + +} diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt new file mode 100644 index 0000000..bbe8044 --- /dev/null +++ b/src/main/kotlin/features/misc/Hud.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.features.misc + +import org.joml.Vector2i +import net.minecraft.client.multiplayer.PlayerInfo +import net.minecraft.network.chat.Component +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr + +object Hud { + val identifier: String + get() = "hud" + + @Config + object TConfig : ManagedConfig(identifier, Category.MISC) { + var dayCount by toggle("day-count") { false } + val dayCountHud by position("day-count-hud", 80, 10) { Vector2i() } + var fpsCount by toggle("fps-count") { false } + val fpsCountHud by position("fps-count-hud", 80, 10) { Vector2i() } + var pingCount by toggle("ping-count") { false } + val pingCountHud by position("ping-count-hud", 80, 10) { Vector2i() } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (TConfig.dayCount) { + it.context.pose().pushMatrix() + TConfig.dayCountHud.applyTransformations(it.context.pose()) + val day = (MC.world?.dayTime ?: 0L) / 24000 + it.context.drawString( + MC.font, + Component.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)), + 36, + MC.font.lineHeight, + -1, + true + ) + it.context.pose().popMatrix() + } + + if (TConfig.fpsCount) { + it.context.pose().pushMatrix() + TConfig.fpsCountHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, Component.literal( + String.format( + tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.fps + ) + ), 36, MC.font.lineHeight, -1, true + ) + it.context.pose().popMatrix() + } + + if (TConfig.pingCount) { + it.context.pose().pushMatrix() + TConfig.pingCountHud.applyTransformations(it.context.pose()) + val ping = MC.player?.let { + val entry: PlayerInfo? = MC.networkHandler?.getPlayerInfo(it.uuid) + entry?.latency ?: -1 + } ?: -1 + it.context.drawString( + MC.font, Component.literal( + String.format( + tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping + ) + ), 36, MC.font.lineHeight, -1, true + ) + + it.context.pose().popMatrix() + } + } +} diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt new file mode 100644 index 0000000..26bec80 --- /dev/null +++ b/src/main/kotlin/features/misc/LicenseViewer.kt @@ -0,0 +1,136 @@ +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 net.minecraft.network.chat.Component +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<License> = listOf(), + val webPresence: String? = null, + val projectName: String, + val projectDescription: String? = null, + val developers: List<Developer> = listOf(), + ) { + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = Component.literal(webPresence ?: "<no web presence>") + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun projectName() = Component.literal(projectName) + + @Bind + fun projectDescription() = Component.literal(projectDescription ?: "<no project description>") + + @get:Bind("developers") + @Transient + val developersO = ObservableList(developers) + + @get:Bind("licenses") + @Transient + val licenses0 = ObservableList(licenses) + } + + @Serializable + data class Developer( + val name: String, + val webPresence: String? = null + ) { + + @Bind("name") + fun nameT() = Component.literal(name) + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = Component.literal(webPresence ?: "<no web presence>") + } + + @Serializable + data class License( + val licenseName: String, + val licenseUrl: String? = null + ) { + @Bind("name") + fun nameG() = Component.literal(licenseName) + + @Bind + fun open() { + MC.openUrl(licenseUrl ?: return) + } + + @Bind + fun hasUrl() = licenseUrl != null + + @Bind + fun url() = Component.literal(licenseUrl ?: "<no link to license text>") + } + + data class LicenseList( + val softwares: List<Software> + ) { + @get:Bind("softwares") + val obs = ObservableList(softwares) + } + + @OptIn(ExperimentalSerializationApi::class) + val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") { + Firmament.json.decodeFromStream<List<Software>?>( + 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/ModAnnouncer.kt b/src/main/kotlin/features/misc/ModAnnouncer.kt new file mode 100644 index 0000000..c2aa013 --- /dev/null +++ b/src/main/kotlin/features/misc/ModAnnouncer.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.misc + +import io.netty.buffer.ByteBuf +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.JoinServerEvent + +/** + * This is a class that announces all mods to hypixel (or any other server you connect to). + * + * I don't mind cheat mods, i just think they should be honest about themselves. + * + * If you are a cheat mod (or think you could possibly be seen as one), you have two options: + * + * - risk getting your users banned from hypixel by exposing your modid + version number in a packet sent on first connect + * - hide yourself from this list by either a mixin, or (more easily) by specifying a tag in your fabric.mod.json: + * + * ```json + * { + * "schemaVersion": 1, + * "id": "my-cheat-mod", + * "custom": { "firmament:hide_from_modlist": true } + * } + * ``` + */ +object ModAnnouncer { + + data class ModEntry( + val modid: String, + val modVersion: String, + ) { + companion object { + val CODEC: StreamCodec<ByteBuf, ModEntry> = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, ModEntry::modid, + ByteBufCodecs.STRING_UTF8, ModEntry::modVersion, + ::ModEntry + ) + } + } + + data class ModPacket( + val mods: List<ModEntry>, + ) : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type<out ModPacket> { + return ID + } + + companion object { + val ID = CustomPacketPayload.Type<ModPacket>(Firmament.identifier("mod_list")) + val CODEC: StreamCodec<ByteBuf, ModPacket> = ModEntry.CODEC.apply(ByteBufCodecs.list()) + .map(::ModPacket, ModPacket::mods) + } + } + + @Subscribe + fun onServerJoin(event: JoinServerEvent) { + val packet = ModPacket( + FabricLoader.getInstance() + .allMods + .filter { !it.metadata.containsCustomValue("firmament:hide_from_modlist") } + .map { ModEntry(it.metadata.id, it.metadata.version.friendlyString) }) + val pbb = PacketByteBufs.create() + ModPacket.CODEC.encode(pbb, packet) + if (pbb.writerIndex() > ServerboundCustomPayloadPacket.MAX_PAYLOAD_SIZE) + return + + event.networkHandler.send(event.packetSender.createPacket(packet)) + } + + init { + PayloadTypeRegistry.playC2S().register(ModPacket.ID, ModPacket.CODEC) + } +} diff --git a/src/main/kotlin/features/notifications/Notifications.kt b/src/main/kotlin/features/notifications/Notifications.kt deleted file mode 100644 index 8d912d1..0000000 --- a/src/main/kotlin/features/notifications/Notifications.kt +++ /dev/null @@ -1,7 +0,0 @@ - -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..3597d6d --- /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.network.chat.Component +import net.minecraft.core.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<ColeWeightWaypoint> { + return waypoints.waypoints.map { + ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z) + } + } + + fun intoFirm(waypoints: List<ColeWeightWaypoint>, 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) -> Component, + ) { + val waypoints = Waypoints.useNonEmptyWaypoints() + ?.let { fromFirm(it, origin) } + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return + } + val data = + Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + fun importAndInform( + source: DefaultSource, + pos: BlockPos?, + positiveFeedback: (Int) -> Component + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ZERO) } + 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.ZERO) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPosition() ?: BlockPos.ZERO) { + 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!!.blockPosition()) { + 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<List<ColeWeightWaypoint>> { + return runCatching { + Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string) + } + } +} diff --git a/src/main/kotlin/features/world/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt index 1263074..b073726 100644 --- a/src/main/kotlin/features/world/FairySouls.kt +++ b/src/main/kotlin/features/world/FairySouls.kt @@ -1,131 +1,123 @@ - - 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.Config +import moe.nea.firmament.util.data.ManagedConfig 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<SkyBlockIsland, MutableSet<Int>> = mutableMapOf() - ) - - override val config: ManagedConfig - get() = TConfig - - object DConfig : ProfileSpecificDataHolder<Data>(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<Coordinate> = emptyList() - var currentMissingSouls: List<Coordinate> = 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<Coordinate> = - 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, 0x80FFFF00.toInt()) - } - 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() - } +object FairySouls { + + + @Serializable + data class Data( + val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf() + ) + + @Config + object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data) + + @Config + object TConfig : ManagedConfig("fairy-souls", Category.MISC) { + val displaySouls by toggle("show") { false } + val resetSouls by button("reset") { + DConfig.data?.foundSouls?.clear() != null + updateMissingSouls() + } + } + + + val identifier: String get() = "fairy-souls" + + val playerReach = 5 + val playerReachSquared = playerReach * playerReach + + var currentLocationName: SkyBlockIsland? = null + var currentLocationSouls: List<Coordinate> = emptyList() + var currentMissingSouls: List<Coordinate> = 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.position + val location = SBData.skyblockLocation ?: return null + val soulLocations: List<Coordinate> = + RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null + return soulLocations + .map { it to it.blockPos.distToCenterSqr(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) + } + 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..c6e2610 --- /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.network.chat.Component +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.DataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DConfig : DataHolder<MutableMap<String, FirmWaypoints>>(serializer(), "waypoints", ::mutableMapOf) + + 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: (Component) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPosition() + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.immutable() + 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!!.blockPosition() + 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 { DConfig.data.keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DConfig.data[get(name)] = exportableWaypoints + DConfig.markDirty() + 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 { DConfig.data.keys } + thenExecute { + val name = get(name) + val waypoints = DConfig.data[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<FirmWaypoints>(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..0b018cb --- /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.core.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<Waypoint>, + 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/NavigableWaypoint.kt b/src/main/kotlin/features/world/NavigableWaypoint.kt index 28a517f..653fd87 100644 --- a/src/main/kotlin/features/world/NavigableWaypoint.kt +++ b/src/main/kotlin/features/world/NavigableWaypoint.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.features.world import io.github.moulberry.repo.data.NEUItem -import net.minecraft.util.math.BlockPos +import net.minecraft.core.BlockPos import moe.nea.firmament.util.SkyBlockIsland abstract class NavigableWaypoint { diff --git a/src/main/kotlin/features/world/NavigationHelper.kt b/src/main/kotlin/features/world/NavigationHelper.kt index acdfb86..ea886bf 100644 --- a/src/main/kotlin/features/world/NavigationHelper.kt +++ b/src/main/kotlin/features/world/NavigationHelper.kt @@ -1,10 +1,10 @@ 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 net.minecraft.network.chat.Component +import net.minecraft.core.BlockPos +import net.minecraft.core.Position +import net.minecraft.core.Vec3i import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.SkyblockServerUpdateEvent import moe.nea.firmament.events.TickEvent @@ -72,7 +72,7 @@ object NavigationHelper { 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) { + if (p.distanceToSqr(tp.position.center) < 5 * 5) { targetWaypoint = null } } @@ -84,11 +84,11 @@ object NavigationHelper { RenderInWorldContext.renderInWorld(event) { if (nt != null) { waypoint(nt.blockPos, - Text.literal("Teleporter to " + nt.toIsland.userFriendlyName), - Text.literal("(towards " + tp.name + "§f)")) + Component.literal("Teleporter to " + nt.toIsland.userFriendlyName), + Component.literal("(towards " + tp.name + "§f)")) } else if (tp.island == SBData.skyblockLocation) { waypoint(tp.position, - Text.literal(tp.name)) + Component.literal(tp.name)) } } } @@ -96,7 +96,7 @@ object NavigationHelper { fun tryWarpNear() { val tp = targetWaypoint if (tp == null) { - MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first.")) + MC.sendChat(Component.literal("Could not find a waypoint to warp you to. Select one first.")) return } WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView()) @@ -106,15 +106,15 @@ object NavigationHelper { fun Vec3i.asPositionView(): Position { return object : Position { - override fun getX(): Double { + override fun x(): Double { return this@asPositionView.x.toDouble() } - override fun getY(): Double { + override fun y(): Double { return this@asPositionView.y.toDouble() } - override fun getZ(): Double { + override fun z(): Double { return this@asPositionView.z.toDouble() } } diff --git a/src/main/kotlin/features/world/NpcWaypointGui.kt b/src/main/kotlin/features/world/NpcWaypointGui.kt index 6146e50..3bae3e8 100644 --- a/src/main/kotlin/features/world/NpcWaypointGui.kt +++ b/src/main/kotlin/features/world/NpcWaypointGui.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.features.world import io.github.notenoughupdates.moulconfig.observer.ObservableList import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.network.chat.Component import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce import moe.nea.firmament.keybindings.SavedKeyBinding @@ -11,7 +12,7 @@ class NpcWaypointGui( data class NavigableWaypointW(val waypoint: NavigableWaypoint) { @Bind - fun name() = waypoint.name + fun name() = Component.literal(waypoint.name) @Bind fun isSelected() = NavigationHelper.targetWaypoint == waypoint diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..ac1600e --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,72 @@ +package moe.nea.firmament.features.world + +import me.shedaniel.math.Color +import kotlin.time.Duration.Companion.seconds +import net.minecraft.network.chat.Component +import net.minecraft.core.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<String, TemporaryWaypoint>() + 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?.listedOnlinePlayers?.find { it.profile.name == player }?.skin?.body + withFacingThePlayer(waypoint.pos.center) { + waypoint(waypoint.pos, Component.translatableEscape("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin.id(), 16, 16, // TODO: 1.21.10 test + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin.id(), 16, 16, // TODO: 1.21.10 + 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 index 16db059..9097d3a 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,150 +2,129 @@ package moe.nea.firmament.features.world import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource -import kotlinx.serialization.Serializable -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds -import net.minecraft.command.argument.BlockPosArgumentType -import net.minecraft.server.command.CommandOutput -import net.minecraft.server.command.ServerCommandSource -import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Vec3d -import moe.nea.firmament.Firmament +import net.minecraft.commands.arguments.coordinates.BlockPosArgument +import net.minecraft.network.chat.Component +import net.minecraft.world.phys.Vec3 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.ProcessChatEvent 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.ClipboardUtils import moe.nea.firmament.util.MC -import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +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 +object Waypoints { + val identifier: String get() = "waypoints" + @Config 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 } - data class TemporaryWaypoint( - val pos: BlockPos, - val postedAt: TimeMark, - ) - - override val config get() = TConfig - - val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() - val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() - - val waypoints = mutableListOf<BlockPos>() - var ordered = false + var waypoints: FirmWaypoints? = null var orderedIndex = 0 - @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, - ) - @Subscribe fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { - if (waypoints.isEmpty()) return + val w = useNonEmptyWaypoints() ?: return RenderInWorldContext.renderInWorld(event) { - if (!ordered) { - waypoints.withIndex().forEach { - block(it.value, 0x800050A0.toInt()) - if (TConfig.showIndex) - withFacingThePlayer(it.value.toCenterPos()) { - text(Text.literal(it.index.toString())) - } + 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.center) { + text(Component.literal(it.index.toString())) + } } } else { - orderedIndex %= waypoints.size + orderedIndex %= w.waypoints.size val firstColor = Color.ofRGBA(0, 200, 40, 180) - color(firstColor) - tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f) - waypoints.withIndex().toList() - .wrappingWindow(orderedIndex, 3) - .zip( - listOf( - firstColor, - Color.ofRGBA(180, 200, 40, 150), - Color.ofRGBA(180, 80, 20, 140), - ) + tracer(w.waypoints[orderedIndex].blockPos.center, color = firstColor.color, 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, col.color) - if (TConfig.showIndex) - withFacingThePlayer(pos.toCenterPos()) { - text(Text.literal(index.toString())) - } + ).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.center) { + text(Component.literal(index.toString())) } + } } } } @Subscribe fun onTick(event: TickEvent) { - if (waypoints.isEmpty() || !ordered) return - orderedIndex %= waypoints.size - val p = MC.player?.pos ?: return + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size + val p = MC.player?.position ?: return if (TConfig.skipToNearest) { orderedIndex = - (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size + (w.waypoints.withIndex().minBy { it.value.blockPos.distToCenterSqr(p) }.index + 1) % w.waypoints.size + } else { - if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { - orderedIndex = (orderedIndex + 1) % waypoints.size + if (w.waypoints[orderedIndex].blockPos.closerToCenterThan(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 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() - ) + fun onWorldSwap(event: WorldReadyEvent) { + if (TConfig.resetWaypointOrderOnWorldSwap) { + orderedIndex = 0 } } @Subscribe fun onCommand(event: CommandEvent.SubCommand) { event.subcommand("waypoint") { - thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> + thenArgument("pos", BlockPosArgument.blockPos()) { pos -> thenExecute { - val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) - waypoints.add(position) + source + val position = pos.get(this).getBlockPos(source.asFakeServer()) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.command.waypoint.added", position.x, position.y, @@ -155,31 +134,75 @@ object Waypoints : FirmamentFeature { } } } - event.subcommand("waypoints") { + 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.clear() - source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) + waypoints = null + source.sendFeedback(Component.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { - ordered = !ordered - if (ordered) { - val p = MC.player?.pos ?: Vec3d.ZERO - orderedIndex = - waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { + val p = MC.player?.position ?: Vec3.ZERO + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.distToCenterSqr(p) }?.index ?: 0 } - source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) + source.sendFeedback(Component.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) } } thenLiteral("skip") { thenExecute { - if (ordered && waypoints.isNotEmpty()) { - orderedIndex = (orderedIndex + 1) % waypoints.size - source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size + source.sendFeedback(Component.translatable("firmament.command.waypoint.skip")) } else { - source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) + source.sendError(Component.translatable("firmament.command.waypoint.skip.error")) } } } @@ -187,79 +210,35 @@ object Waypoints : FirmamentFeature { thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> thenExecute { val index = get(indexArg) - if (index in waypoints.indices) { - waypoints.removeAt(index) - source.sendFeedback(Text.stringifiedTranslatable( - "firmament.command.waypoint.remove", - index)) + val w = useNonEmptyWaypoints() + if (w != null && index in w.waypoints.indices) { + w.waypoints.removeAt(index) + source.sendFeedback( + Component.translatableEscape( + "firmament.command.waypoint.remove", + index + ) + ) } else { - source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) + source.sendError(Component.translatableEscape("firmament.command.waypoint.remove.error")) } } } } - thenLiteral("import") { - thenExecute { - val contents = ClipboardUtils.getTextContents() - val data = try { - Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents) - } catch (ex: Exception) { - Firmament.logger.error("Could not load waypoints from clipboard", ex) - source.sendError(Text.translatable("firmament.command.waypoint.import.error")) - return@thenExecute - } - waypoints.clear() - data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) } - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.import", - data.size - ) - ) - } - } } } - @Subscribe - fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { - temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } - if (temporaryPlayerWaypointList.isEmpty()) return - RenderInWorldContext.renderInWorld(event) { - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - block(waypoint.pos, 0xFFFFFF00.toInt()) - } - 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() - } + fun textInvalidIndex(index: Int) = + tr( + "firmament.command.waypoint.invalid-index", + "Invalid index $index provided." + ) + + fun textNothingToExport(): Component = + tr( + "firmament.command.waypoint.export.nowaypoints", + "No waypoints to export found. Add some with /firm waypoint ~ ~ ~." + ) } fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { @@ -272,35 +251,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { } return result } - - -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/gui/BarComponent.kt b/src/main/kotlin/gui/BarComponent.kt index b82c666..4c0a52d 100644 --- a/src/main/kotlin/gui/BarComponent.kt +++ b/src/main/kotlin/gui/BarComponent.kt @@ -1,16 +1,14 @@ 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext import me.shedaniel.math.Color -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderLayer -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament class BarComponent( @@ -27,13 +25,13 @@ class BarComponent( } data class Texture( - val identifier: Identifier, - val u1: Float, val v1: Float, - val u2: Float, val v2: Float, + val identifier: ResourceLocation, + 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, + fun draw(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int, color: Color) { + context.innerBlit( + RenderPipelines.GUI_TEXTURED, identifier, x, y, x + width, x + height, u1, u2, v1, v2, @@ -51,13 +49,13 @@ class BarComponent( } private fun drawSection( - context: DrawContext, - texture: Texture, - x: Int, - y: Int, - width: Int, - sectionStart: Double, - sectionEnd: Double + context: GuiGraphics, + 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) @@ -81,7 +79,7 @@ class BarComponent( } override fun render(context: GuiImmediateContext) { - val renderContext = (context.renderContext as ModernRenderContext).drawContext + val renderContext = (context.renderContext as MoulConfigRenderContext).drawContext var i = 0 val x = 0 val y = 0 @@ -104,20 +102,11 @@ class BarComponent( (context.width - 4) * total.get() / context.width, total.get() ) - RenderSystem.setShaderColor(1F, 1F, 1F, 1F) } } -fun Identifier.toMoulConfig(): MyResourceLocation { +fun ResourceLocation.toMoulConfig(): MyResourceLocation { return MyResourceLocation(this.namespace, this.path) } - -fun RenderContext.color(color: Color) { - color(color.red, color.green, color.blue, color.alpha) -} - -fun RenderContext.color(red: Int, green: Int, blue: Int, alpha: Int) { - color(red / 255f, green / 255f, blue / 255f, alpha / 255f) -} diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt index 761c086..da3e5c8 100644 --- a/src/main/kotlin/gui/CheckboxComponent.kt +++ b/src/main/kotlin/gui/CheckboxComponent.kt @@ -4,8 +4,8 @@ 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext +import net.minecraft.client.renderer.RenderPipelines import moe.nea.firmament.Firmament class CheckboxComponent<T>( @@ -25,11 +25,11 @@ class CheckboxComponent<T>( } override fun render(context: GuiImmediateContext) { - val ctx = (context.renderContext as ModernRenderContext).drawContext - ctx.drawGuiTexture( - RenderLayer::getGuiTextured, - if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked") - else Firmament.identifier("firmament:widget/checkbox_unchecked"), + val ctx = (context.renderContext as MoulConfigRenderContext).drawContext + ctx.blitSprite( + RenderPipelines.GUI_TEXTURED, + if (isEnabled()) Firmament.identifier("widget/checkbox_checked") + else Firmament.identifier("widget/checkbox_unchecked"), 0, 0, 16, 16 ) @@ -43,6 +43,7 @@ class CheckboxComponent<T>( isClicking = false if (context.isHovered) state.set(value) + blur() return true } if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) { diff --git a/src/main/kotlin/gui/FirmButtonComponent.kt b/src/main/kotlin/gui/FirmButtonComponent.kt index 82e5b05..1469c09 100644 --- a/src/main/kotlin/gui/FirmButtonComponent.kt +++ b/src/main/kotlin/gui/FirmButtonComponent.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.gui import io.github.notenoughupdates.moulconfig.common.MyResourceLocation @@ -11,71 +10,78 @@ import io.github.notenoughupdates.moulconfig.observer.GetSetter open class FirmButtonComponent( - child: GuiComponent, - val isEnabled: GetSetter<Boolean> = GetSetter.constant(true), - val noBackground: Boolean = false, - val action: Runnable, + child: GuiComponent, + val isEnabled: GetSetter<Boolean> = GetSetter.constant(true), + val noBackground: Boolean = false, + val action: (mouseButton: Int) -> Unit, ) : 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 - } + constructor( + child: GuiComponent, + isEnabled: GetSetter<Boolean> = GetSetter.constant(true), + noBackground: Boolean = false, + action: Runnable, + ) : this(child, isEnabled, noBackground, { action.run() }) + + /* 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) { + isClicking = false + if (context.isHovered) { + action.invoke(mouseEvent.mouseButton) + } + return true + } + } + if (!context.isHovered) return false + if (mouseEvent !is MouseEvent.Click) return false + if (mouseEvent.mouseState) { + requestFocus() + isClicking = true + return true + } + return false + } - open fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> = - if (!isEnabled.get()) disabledBg - else if (context.isHovered || isClicking) hoveredBg - else unhoveredBg + open fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> = + 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(), 0f) - element.render(getChildContext(context)) - context.renderContext.popMatrix() - } + 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 index b1792ce..eed795a 100644 --- a/src/main/kotlin/gui/FirmHoverComponent.kt +++ b/src/main/kotlin/gui/FirmHoverComponent.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.gui +import io.github.notenoughupdates.moulconfig.common.text.StructuredText import io.github.notenoughupdates.moulconfig.gui.GuiComponent import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent @@ -10,50 +11,51 @@ import kotlin.time.Duration import moe.nea.firmament.util.TimeMark class FirmHoverComponent( - val child: GuiComponent, - val hoverLines: Supplier<List<String>>, - val hoverDelay: Duration, + val child: GuiComponent, + val hoverLines: Supplier<List<String>>, + val hoverDelay: Duration, ) : GuiComponent() { - override fun getWidth(): Int { - return child.width - } - - override fun getHeight(): Int { - return child.height - } - - override fun <T : Any?> foldChildren( - initial: T, - visitor: BiFunction<GuiComponent, T, T> - ): T { - return visitor.apply(child, initial) - } - - override fun render(context: GuiImmediateContext) { - if (context.isHovered && (permaHover || lastMouseMove.passedTime() > hoverDelay)) { - context.renderContext.scheduleDrawTooltip(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) - } + override fun getWidth(): Int { + return child.width + } + + override fun getHeight(): Int { + return child.height + } + + override fun <T : Any?> foldChildren( + initial: T, + visitor: BiFunction<GuiComponent, T, T> + ): 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() + .map { it -> StructuredText.of(it) }) + 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/ImageComponent.kt b/src/main/kotlin/gui/ImageComponent.kt index bba7dee..695c0ed 100644 --- a/src/main/kotlin/gui/ImageComponent.kt +++ b/src/main/kotlin/gui/ImageComponent.kt @@ -6,28 +6,30 @@ 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<MyResourceLocation>, - val u1: Float, - val u2: Float, - val v1: Float, - val v2: Float, + private val width: Int, + private val height: Int, + val resourceLocation: Supplier<MyResourceLocation>, + val u1: Float, + val u2: Float, + val v1: Float, + val v2: Float, ) : GuiComponent() { - override fun getWidth(): Int { - return width - } + override fun getWidth(): Int { + return width + } - override fun getHeight(): Int { - return height - } + override fun getHeight(): Int { + return height + } - override fun render(context: GuiImmediateContext) { - context.renderContext.bindTexture(resourceLocation.get()) - context.renderContext.drawTexturedRect( - 0f, 0f, - context.width.toFloat(), context.height.toFloat(), - u1, v1, u2, v2 - ) - } + 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/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt index 73ff444..60711ca 100644 --- a/src/main/kotlin/gui/config/AllConfigsGui.kt +++ b/src/main/kotlin/gui/config/AllConfigsGui.kt @@ -2,11 +2,19 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component +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 +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig object AllConfigsGui { // @@ -15,9 +23,11 @@ object AllConfigsGui { // RepoManager.Config // ) + FeatureManager.allFeatures.mapNotNull { it.config } + @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 <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this) @@ -27,16 +37,16 @@ object AllConfigsGui { val configs = category.configs.map { EntryMapping(it) }.toObservableList() @Bind - fun name() = category.labelText.string + fun name() = category.labelText @Bind fun close() { - MC.screen?.close() + MC.screen?.onClose() } class EntryMapping(val config: ManagedConfig) { @Bind - fun name() = Text.translatable("firmament.config.${config.name}").string + fun name() = Component.translatable("firmament.config.${config.name}") @Bind fun openEditor() { @@ -53,7 +63,7 @@ object AllConfigsGui { class CategoryEntry(val category: ManagedConfig.Category) { @Bind - fun name() = category.labelText.string + fun name() = category.labelText @Bind fun open() { @@ -66,7 +76,7 @@ object AllConfigsGui { return MoulConfigUtils.loadScreen("config/main", CategoryView(), parent) } - fun makeScreen(parent: Screen? = null): Screen { + fun makeScreen(search: String? = null, parent: Screen? = null): Screen { val wantedKey = when { ConfigConfig.enableMoulConfig -> "moulconfig" ConfigConfig.enableYacl -> "yacl" @@ -74,10 +84,23 @@ object AllConfigsGui { } val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey } ?: FirmamentConfigScreenProvider.providers.first() - return provider.open(parent) + 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 index 8592777..b954401 100644 --- a/src/main/kotlin/gui/config/BooleanHandler.kt +++ b/src/main/kotlin/gui/config/BooleanHandler.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.jsonPrimitive +import moe.nea.firmament.util.data.ManagedConfig class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Boolean> { override fun toJson(element: Boolean): JsonElement? { @@ -29,7 +30,7 @@ class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Bo override fun set(newValue: Boolean) { opt.set(newValue) - config.save() + config.markDirty() } }, 200) )) diff --git a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt index 19e7383..6495e68 100644 --- a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt +++ b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt @@ -1,14 +1,14 @@ package moe.nea.firmament.gui.config import com.google.auto.service.AutoService -import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screens.Screen @AutoService(FirmamentConfigScreenProvider::class) class BuiltInConfigScreenProvider : FirmamentConfigScreenProvider { override val key: String get() = "builtin" - override fun open(parent: Screen?): Screen { + 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 index 2ea3efc..494d08a 100644 --- a/src/main/kotlin/gui/config/ChoiceHandler.kt +++ b/src/main/kotlin/gui/config/ChoiceHandler.kt @@ -7,16 +7,17 @@ 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 net.minecraft.util.StringRepresentable import moe.nea.firmament.gui.CheckboxComponent import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.json.KJsonOps class ChoiceHandler<E>( val enumClass: Class<E>, val universe: List<E>, -) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable { - val codec = StringIdentifiable.createCodec { +) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringRepresentable { + val codec = StringRepresentable.fromEnum { @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") (universe as java.util.List<*>).toArray(arrayOfNulls<Enum<E>>(0)) as Array<E> } diff --git a/src/main/kotlin/gui/config/ClickHandler.kt b/src/main/kotlin/gui/config/ClickHandler.kt index fa1c621..9ea83aa 100644 --- a/src/main/kotlin/gui/config/ClickHandler.kt +++ b/src/main/kotlin/gui/config/ClickHandler.kt @@ -5,6 +5,7 @@ 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 +import moe.nea.firmament.util.data.ManagedConfig class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler<Unit> { override fun toJson(element: Unit): JsonElement? { diff --git a/src/main/kotlin/gui/config/ColourHandler.kt b/src/main/kotlin/gui/config/ColourHandler.kt new file mode 100644 index 0000000..33daa6d --- /dev/null +++ b/src/main/kotlin/gui/config/ColourHandler.kt @@ -0,0 +1,83 @@ +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 +import moe.nea.firmament.util.data.ManagedConfig + +class ColourHandler(val config: ManagedConfig) : + ManagedConfig.OptionHandler<ChromaColour> { + @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<ChromaColour> { + 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<ChromaColour>, + guiAppender: GuiAppender + ) { + guiAppender.appendLabeledRow( + opt.labelText, + ColorSelectComponent( + 0, + 0, + opt.value.toLegacyString(), + { + opt.value = ChromaColour.forLegacyString(it) + config.markDirty() + }, + { } + ) + ) + } +} diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt index 8d485b1..0fc945f 100644 --- a/src/main/kotlin/gui/config/DurationHandler.kt +++ b/src/main/kotlin/gui/config/DurationHandler.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.gui.config import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.text.StructuredText import io.github.notenoughupdates.moulconfig.gui.component.RowComponent import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent import io.github.notenoughupdates.moulconfig.gui.component.TextComponent @@ -14,8 +15,8 @@ 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 +import moe.nea.firmament.util.data.ManagedConfig class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Duration) : ManagedConfig.OptionHandler<Duration> { @@ -31,8 +32,8 @@ class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Dur guiAppender.appendLabeledRow( opt.labelText, RowComponent( - TextComponent(IMinecraft.instance.defaultFontRenderer, - { FirmFormatters.formatTimespan(opt.value) }, + TextComponent(IMinecraft.INSTANCE.defaultFontRenderer, + { StructuredText.of(FirmFormatters.formatTimespan(opt.value)) }, 40, TextComponent.TextAlignment.CENTER, true, diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt index 3b80b7e..a2dee69 100644 --- a/src/main/kotlin/gui/config/EnumRenderer.kt +++ b/src/main/kotlin/gui/config/EnumRenderer.kt @@ -1,14 +1,14 @@ package moe.nea.firmament.gui.config -import net.minecraft.text.Text +import net.minecraft.network.chat.Component interface EnumRenderer<E : Any> { - fun getName(option: ManagedOption<E>, value: E): Text + fun getName(option: ManagedOption<E>, value: E): Component companion object { fun <E : Enum<E>> default() = object : EnumRenderer<E> { - override fun getName(option: ManagedOption<E>, value: E): Text { - return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase()) + override fun getName(option: ManagedOption<E>, value: E): Component { + return Component.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 index faad1cc..d2a8ab6 100644 --- a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt +++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt @@ -1,13 +1,13 @@ package moe.nea.firmament.gui.config -import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screens.Screen import moe.nea.firmament.util.compatloader.CompatLoader interface FirmamentConfigScreenProvider { val key: String val isEnabled: Boolean get() = true - fun open(parent: Screen?): Screen + fun open(search: String?, parent: Screen?): Screen companion object : CompatLoader<FirmamentConfigScreenProvider>(FirmamentConfigScreenProvider::class) { val providers by lazy { diff --git a/src/main/kotlin/gui/config/GuiAppender.kt b/src/main/kotlin/gui/config/GuiAppender.kt index 329319d..ba28400 100644 --- a/src/main/kotlin/gui/config/GuiAppender.kt +++ b/src/main/kotlin/gui/config/GuiAppender.kt @@ -6,8 +6,8 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component import moe.nea.firmament.gui.FixedComponent class GuiAppender(val width: Int, val screenAccessor: () -> Screen) { @@ -18,7 +18,7 @@ class GuiAppender(val width: Int, val screenAccessor: () -> Screen) { reloadables.add(reloadable) } - fun appendLabeledRow(label: Text, right: GuiComponent) { + fun appendLabeledRow(label: Component, right: GuiComponent) { appendSplitRow( TextComponent(label.string), right diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt index a9659ee..915dcf3 100644 --- a/src/main/kotlin/gui/config/HudMetaHandler.kt +++ b/src/main/kotlin/gui/config/HudMetaHandler.kt @@ -5,21 +5,29 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.MutableComponent +import net.minecraft.network.chat.Component +import moe.nea.firmament.Firmament import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.jarvis.JarvisIntegration import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.ManagedConfig -class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val width: Int, val height: Int) : +class HudMetaHandler( + val config: ManagedConfig, + val propertyName: String, + val label: MutableComponent, + val width: Int, + val height: Int +) : ManagedConfig.OptionHandler<HudMeta> { 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) + return HudMeta(Json.decodeFromJsonElement(element), Firmament.identifier(propertyName), label, width, height) } fun openEditor(option: ManagedOption<HudMeta>, oldScreen: Screen) { @@ -34,7 +42,8 @@ class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val widt opt.labelText, FirmButtonComponent( TextComponent( - Text.stringifiedTranslatable("firmament.hud.edit", label).string), + Component.translatableEscape("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 index 31ce90f..ab0237a 100644 --- a/src/main/kotlin/gui/config/IntegerHandler.kt +++ b/src/main/kotlin/gui/config/IntegerHandler.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.gui.config import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.text.StructuredText import io.github.notenoughupdates.moulconfig.gui.component.RowComponent import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent import io.github.notenoughupdates.moulconfig.gui.component.TextComponent @@ -12,6 +13,7 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.data.ManagedConfig class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler<Int> { override fun toJson(element: Int): JsonElement? { @@ -26,8 +28,8 @@ class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : Ma guiAppender.appendLabeledRow( opt.labelText, RowComponent( - TextComponent(IMinecraft.instance.defaultFontRenderer, - { FirmFormatters.formatCommas(opt.value, 0) }, + TextComponent(IMinecraft.INSTANCE.defaultFontRenderer, + { StructuredText.of(FirmFormatters.formatCommas(opt.value, 0)) }, 40, TextComponent.TextAlignment.CENTER, true, diff --git a/src/main/kotlin/gui/config/JAnyHud.kt b/src/main/kotlin/gui/config/JAnyHud.kt index 35c4eb2..63975c6 100644 --- a/src/main/kotlin/gui/config/JAnyHud.kt +++ b/src/main/kotlin/gui/config/JAnyHud.kt @@ -1,48 +1,67 @@ - - package moe.nea.firmament.gui.config import moe.nea.jarvis.api.JarvisHud -import moe.nea.jarvis.api.JarvisScalable +import org.joml.Matrix3x2f +import org.joml.Vector2i +import org.joml.Vector2ic import kotlinx.serialization.Serializable -import net.minecraft.text.Text +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.jarvis.JarvisIntegration @Serializable data class HudPosition( - var x: Double, - var y: Double, - var scale: Float, + var x: Int, + var y: Int, + var scale: Float, ) data class HudMeta( val position: HudPosition, - private val label: Text, + private val id: ResourceLocation, + private val label: Component, private val width: Int, private val height: Int, -) : JarvisScalable, JarvisHud { - override fun getX(): Double = position.x +) : JarvisHud, JarvisHud.Scalable { + override fun getLabel(): Component = label + override fun getUnscaledWidth(): Int { + return width + } + + override fun getUnscaledHeight(): Int { + return height + } - override fun setX(newX: Double) { - position.x = newX - } + override fun getHudId(): ResourceLocation { + return id + } - override fun getY(): Double = position.y + override fun getPosition(): Vector2ic { + return Vector2i(position.x, position.y) + } - override fun setY(newY: Double) { - position.y = newY - } + override fun setPosition(p0: Vector2ic) { + position.x = p0.x() + position.y = p0.y() + } - override fun getLabel(): Text = label + override fun isEnabled(): Boolean { + return true // TODO: this should be actually truthful, if possible + } - override fun getWidth(): Int = width + override fun isVisible(): Boolean { + return true // TODO: this should be actually truthful, if possible + } - override fun getHeight(): Int = height + override fun getScale(): Float = position.scale - override fun getScale(): Float = position.scale + override fun setScale(newScale: Float) { + position.scale = newScale + } - override fun setScale(newScale: Float) { - position.scale = newScale - } + fun applyTransformations(matrix4f: Matrix3x2f) { + applyTransformations(JarvisIntegration.jarvis, matrix4f) + } } diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt index d7d0b47..3c08da2 100644 --- a/src/main/kotlin/gui/config/KeyBindingHandler.kt +++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt @@ -1,11 +1,5 @@ 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 @@ -13,6 +7,7 @@ import kotlinx.serialization.json.encodeToJsonElement import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.keybindings.FirmamentKeyBindings import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.data.ManagedConfig class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : ManagedConfig.OptionHandler<SavedKeyBinding> { @@ -35,39 +30,12 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : { opt.value }, { opt.value = it - opt.element.save() + opt.element.markDirty() }, { button.blur() }, { button.requestFocus() } ) - button = object : FirmButtonComponent( - TextComponent( - IMinecraft.instance.defaultFontRenderer, - { sm.label.string }, - 130, - TextComponent.TextAlignment.LEFT, - false, - false - ), action = { - sm.onClick() - }) { - override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { - if (event is KeyboardEvent.KeyPressed) { - return sm.keyboardEvent(event.keycode, event.pressed) - } - return super.keyboardEvent(event, context) - } - - override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> { - if (sm.editing) return activeBg - return super.getBackground(context) - } - - - override fun onLostFocus() { - sm.onLostFocus() - } - } + button = sm.createButton() sm.updateLabel() return button } diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt index cc8178d..9cf2771 100644 --- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt +++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt @@ -1,8 +1,18 @@ 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform import org.lwjgl.glfw.GLFW -import net.minecraft.text.Text -import net.minecraft.util.Formatting +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.GenericInputButton +import moe.nea.firmament.keybindings.InputModifiers import moe.nea.firmament.keybindings.SavedKeyBinding class KeyBindingStateManager( @@ -12,73 +22,65 @@ class KeyBindingStateManager( val requestFocus: () -> Unit, ) { var editing = false - var lastPressed = 0 - var lastPressedNonModifier = 0 - var label: Text = Text.literal("") + var lastPressed: GenericInputButton? = null + var label: Component = Component.literal("") - fun onClick() { + fun onClick(mouseButton: Int) { if (editing) { - editing = false - blur() - } else { + keyboardEvent(GenericInputButton.mouse(mouseButton), true) + } else if (mouseButton == GLFW.GLFW_MOUSE_BUTTON_LEFT) { editing = true requestFocus() } updateLabel() } - fun keyboardEvent(keyCode: Int, pressed: Boolean): Boolean { - return if (pressed) onKeyPressed(keyCode, SavedKeyBinding.getModInt()) - else onKeyReleased(keyCode, SavedKeyBinding.getModInt()) + fun keyboardEvent(keyCode: GenericInputButton, pressed: Boolean): Boolean { + return if (pressed) onKeyPressed(keyCode, InputModifiers.current()) + else onKeyReleased(keyCode, InputModifiers.current()) } - fun onKeyPressed(ch: Int, modifiers: Int): Boolean { + fun onKeyPressed( + ch: GenericInputButton, + modifiers: InputModifiers + ): Boolean { // TODO !!!!!: genericify this method to allow for other inputs if (!editing) { return false } - if (ch == GLFW.GLFW_KEY_ESCAPE) { - lastPressedNonModifier = 0 + if (ch == GenericInputButton.escape()) { editing = false - lastPressed = 0 - setValue(SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)) + lastPressed = null + setValue(SavedKeyBinding.unbound()) 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 - ) { + if (ch.isModifier()) { lastPressed = ch } else { - setValue(SavedKeyBinding( - ch, modifiers - )) + setValue(SavedKeyBinding(ch, modifiers)) editing = false blur() - lastPressed = 0 - lastPressedNonModifier = 0 + lastPressed = null } updateLabel() return true } fun onLostFocus() { - lastPressedNonModifier = 0 editing = false - lastPressed = 0 + lastPressed = null updateLabel() } - fun onKeyReleased(ch: Int, modifiers: Int): Boolean { + fun onKeyReleased(ch: GenericInputButton, modifiers: InputModifiers): Boolean { if (!editing) return false - if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) { + if (ch == lastPressed) { // TODO: check modifiers dont duplicate (CTRL+CTRL) setValue(SavedKeyBinding(ch, modifiers)) editing = false blur() - lastPressed = 0 - lastPressedNonModifier = 0 + lastPressed = null } updateLabel() return true @@ -87,22 +89,51 @@ class KeyBindingStateManager( 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 = Component.empty() + val modifiers = InputModifiers.current() + if (!modifiers.isEmpty()) { + stroke.append(modifiers.format()) + stroke.append(" + ") } stroke.append("???") - stroke.styled { it.withColor(Formatting.YELLOW) } + stroke.withStyle { it.withColor(ChatFormatting.YELLOW) } } label = stroke } + fun createButton(): FirmButtonComponent { + return object : FirmButtonComponent( + TextComponent( + IMinecraft.INSTANCE.defaultFontRenderer, + { MoulConfigPlatform.wrap(this@KeyBindingStateManager.label) }, + 130, + TextComponent.TextAlignment.LEFT, + false, + false + ), action = { + this@KeyBindingStateManager.onClick(it) + }) { + override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { + if (event is KeyboardEvent.KeyPressed) { + return this@KeyBindingStateManager.keyboardEvent( + GenericInputButton.ofKeyAndScan( + event.keycode, + event.scancode + ), event.pressed + ) + } + return super.keyboardEvent(event, context) + } + override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> { + if (this@KeyBindingStateManager.editing) return activeBg + return super.getBackground(context) + } + + + override fun onLostFocus() { + this@KeyBindingStateManager.onLostFocus() + } + } + } } diff --git a/src/main/kotlin/gui/config/ManagedConfigElement.kt b/src/main/kotlin/gui/config/ManagedConfigElement.kt deleted file mode 100644 index 28cd6b8..0000000 --- a/src/main/kotlin/gui/config/ManagedConfigElement.kt +++ /dev/null @@ -1,8 +0,0 @@ - - -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 index 383f392..4c228de 100644 --- a/src/main/kotlin/gui/config/ManagedOption.kt +++ b/src/main/kotlin/gui/config/ManagedOption.kt @@ -5,8 +5,9 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.data.ManagedConfig class ManagedOption<T : Any>( val element: ManagedConfig, @@ -23,15 +24,15 @@ class ManagedOption<T : Any>( } val rawLabelText = "firmament.config.${element.name}.${propertyName}" - val labelText: Text = Text.translatable(rawLabelText) + val labelText: Component = Component.translatable(rawLabelText) val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description" - val labelDescription: Text = Text.translatable(descriptionTranslationKey) + val labelDescription: Component = Component.translatable(descriptionTranslationKey) - private var actualValue: T? = null + var _actualValue: T? = null var value: T - get() = actualValue ?: error("Lateinit variable not initialized") + get() = _actualValue ?: error("Lateinit variable not initialized") set(value) { - actualValue = value + _actualValue = value element.onChange(this) } @@ -49,7 +50,7 @@ class ManagedOption<T : Any>( value = handler.fromJson(root[propertyName]!!) return } catch (e: Exception) { - ErrorUtil.softError( + ErrorUtil.logError( "Exception during loading of config file ${element.name}. This will reset this config.", e ) diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt index a326abb..17bb981 100644 --- a/src/main/kotlin/gui/config/StringHandler.kt +++ b/src/main/kotlin/gui/config/StringHandler.kt @@ -7,7 +7,8 @@ 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 +import net.minecraft.network.chat.Component +import moe.nea.firmament.util.data.ManagedConfig class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> { override fun toJson(element: String): JsonElement? { @@ -25,11 +26,11 @@ class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Str object : GetSetter<String> by opt { override fun set(newValue: String) { opt.set(newValue) - config.save() + config.markDirty() } }, 130, - suggestion = Text.translatableWithFallback(opt.rawLabelText + ".hint", "").string + suggestion = Component.translatableWithFallback(opt.rawLabelText + ".hint", "").string ), ) } diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt new file mode 100644 index 0000000..4a06ec6 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt @@ -0,0 +1,98 @@ +package moe.nea.firmament.gui.config.storage + +import java.io.PrintWriter +import java.nio.file.Path +import org.apache.commons.io.output.StringBuilderWriter +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.OnErrorResult +import kotlin.io.path.Path +import kotlin.io.path.copyToRecursively +import kotlin.io.path.createParentDirectories +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +data class ConfigLoadContext( + val loadId: String, +) : AutoCloseable { + val backupPath = Path("backups").resolve(Firmament.MOD_ID) + .resolve("config-$loadId") + .toAbsolutePath() + val logFile = Path("logs") + .resolve(Firmament.MOD_ID) + .resolve("config-$loadId.log") + .toAbsolutePath() + val logBuffer = StringBuilder() + + var shouldSaveLogBuffer = false + fun markShouldSaveLogBuffer() { + shouldSaveLogBuffer = true + } + + fun logDebug(message: String) { + logBuffer.append("[DEBUG] ").append(message).appendLine() + } + + fun logInfo(message: String) { + if (Firmament.DEBUG) + Firmament.logger.info("[ConfigUpgrade] $message") + logBuffer.append("[INFO] ").append(message).appendLine() + } + + fun logError(message: String, exception: Throwable) { + markShouldSaveLogBuffer() + if (Firmament.DEBUG) + Firmament.logger.error("[ConfigUpgrade] $message", exception) + logBuffer.append("[ERROR] ").append(message).appendLine() + PrintWriter(StringBuilderWriter(logBuffer)).use { + exception.printStackTrace(it) + } + logBuffer.appendLine() + } + + fun logError(message: String) { + markShouldSaveLogBuffer() + Firmament.logger.error("[ConfigUpgrade] $message") + logBuffer.append("[ERROR] ").append(message).appendLine() + } + + fun ensureWritable(path: Path) { + path.createParentDirectories() + } + + fun use(block: (ConfigLoadContext) -> Unit) { + try { + block(this) + } catch (ex: Exception) { + logError("Caught exception on CLC", ex) + } finally { + close() + } + } + + override fun close() { + logInfo("Closing out config load.") + if (shouldSaveLogBuffer) { + try { + ensureWritable(logFile) + logFile.writeText(logBuffer.toString()) + } catch (ex: Exception) { + logError("Could not save config load log", ex) + } + } + } + + @OptIn(ExperimentalPathApi::class) + fun createBackup(folder: Path, string: String) { + val backupDestination = backupPath.resolve("$string-${System.currentTimeMillis()}") + logError("Creating backup of $folder in $backupDestination") + folder.copyToRecursively( + backupDestination.createParentDirectories(), + onError = { source: Path, target: Path, exception: Exception -> + logError("Failed to copy subtree $source to $target", exception) + OnErrorResult.SKIP_SUBTREE + }, + followLinks = false, + overwrite = false + ) + } +} diff --git a/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt new file mode 100644 index 0000000..8258fe7 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.gui.config.storage + +enum class ConfigStorageClass { // TODO: make this encode type info somehow + PROFILE, + STORAGE, + CONFIG, +} + diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt new file mode 100644 index 0000000..0292721 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt @@ -0,0 +1,252 @@ +package moe.nea.firmament.gui.config.storage + +import java.util.UUID +import java.util.concurrent.CompletableFuture +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.time.Duration.Companion.seconds +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.debug.DebugLogger +import moe.nea.firmament.util.SBData.NULL_UUID +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.IConfigProvider +import moe.nea.firmament.util.data.IDataHolder +import moe.nea.firmament.util.data.ProfileKeyedConfig +import moe.nea.firmament.util.json.intoGson +import moe.nea.firmament.util.json.intoKotlinJson + +object FirmamentConfigLoader { + val currentConfigVersion = 1000 + val configFolder = Path("config/firmament") + .toAbsolutePath() + val storageFolder = configFolder.resolve("storage") + val profilePath = configFolder.resolve("profiles") + val tagLines = listOf( + "<- your config version here", + "I'm a teapot", + "mail.example.com ESMTP", + "Apples" + ) + val configVersionFile = configFolder.resolve("config.version") + + fun loadConfig() { + if (configFolder.exists()) { + if (!configVersionFile.exists()) { + LegacyImporter.importFromLegacy() + } + updateConfigs() + } + + ConfigLoadContext("load-${System.currentTimeMillis()}").use { loadContext -> + val configData = FirstLevelSplitJsonFolder(loadContext, configFolder).load() + loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG) + val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load() + loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE) + var profileData = + profilePath.takeIf { it.exists() } + ?.listDirectoryEntries() + ?.filter { it.isDirectory() } + ?.mapNotNull { + val uuid= runCatching { UUID.fromString(it.name) }.getOrNull() ?: return@mapNotNull null + uuid to FirstLevelSplitJsonFolder(loadContext, it).load() + } + ?.toMap() + if (profileData.isNullOrEmpty()) + profileData = mapOf(NULL_UUID to JsonObject(mapOf())) + profileData.forEach { (key, value) -> + loadConfigFromData(value, key, ConfigStorageClass.PROFILE) + } + } + } + + fun <T> loadConfigFromData( + configData: JsonObject, + key: T?, + storageClass: ConfigStorageClass + ) { + for (holder in allConfigs) { + if (holder.storageClass == storageClass) { + val h = (holder as IDataHolder<T>) + if (key == null) { + h.explicitDefaultLoad() + } else { + h.loadFrom(key, configData) + } + } + } + } + + fun <T> collectConfigFromData( + key: T, + storageClass: ConfigStorageClass, + ): JsonObject { + var json = JsonObject(mapOf()) + for (holder in allConfigs) { + if (holder.storageClass == storageClass) { + json = mergeJson(json, (holder as IDataHolder<T>).saveTo(key)) + } + } + return json + } + + fun <T> saveStorage( + storageClass: ConfigStorageClass, + key: T, + firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder, + ) { + firstLevelSplitJsonFolder.save( + collectConfigFromData(key, storageClass) + ) + } + + fun collectAllProfileIds(): Set<UUID> { + return allConfigs + .filter { it.storageClass == ConfigStorageClass.PROFILE } + .flatMapTo(mutableSetOf()) { + (it as ProfileKeyedConfig<*>).keys() + } + } + + fun saveAll() { + ConfigLoadContext("save-${System.currentTimeMillis()}").use { context -> + saveStorage( + ConfigStorageClass.CONFIG, + Unit, + FirstLevelSplitJsonFolder(context, configFolder) + ) + saveStorage( + ConfigStorageClass.STORAGE, + Unit, + FirstLevelSplitJsonFolder(context, storageFolder) + ) + collectAllProfileIds().forEach { profileId -> + saveStorage( + ConfigStorageClass.PROFILE, + profileId, + FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString())) + ) + } + writeConfigVersion() + } + } + + fun mergeJson(a: JsonObject, b: JsonObject): JsonObject { + fun mergeInner(a: JsonElement?, b: JsonElement?): JsonElement { + if (a == null) + return b!! + if (b == null) + return a + a as JsonObject + b as JsonObject + return buildJsonObject { + (a.keys + b.keys) + .forEach { + put(it, mergeInner(a[it], b[it])) + } + } + } + return mergeInner(a, b) as JsonObject + } + + val allConfigs: List<IDataHolder<*>> = IConfigProvider.providers.allValidInstances.flatMap { it.configs } + + fun updateConfigs() { + val startVersion = configVersionFile.readText() + .substringBefore(' ') + .trim() + .toInt() + ConfigLoadContext("update-from-$startVersion-to-$currentConfigVersion-${System.currentTimeMillis()}") + .use { loadContext -> + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.CONFIG, + FirstLevelSplitJsonFolder(loadContext, configFolder) + ) + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.STORAGE, + FirstLevelSplitJsonFolder(loadContext, storageFolder) + ) + profilePath.forEachDirectoryEntry { + updateOneConfig( + loadContext, + startVersion, + ConfigStorageClass.PROFILE, + FirstLevelSplitJsonFolder(loadContext, it) + ) + } + writeConfigVersion() + } + } + + fun writeConfigVersion() { + configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}") + } + + private fun updateOneConfig( + loadContext: ConfigLoadContext, + startVersion: Int, + storageClass: ConfigStorageClass, + firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder + ) { + if (startVersion == currentConfigVersion) { + loadContext.logDebug("Skipping upgrade to ") + return + } + loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion") + var data = firstLevelSplitJsonFolder.load() + for (nextVersion in (startVersion + 1)..currentConfigVersion) { + data = updateOneConfigOnce(nextVersion, storageClass, data) + } + firstLevelSplitJsonFolder.save(data) + } + + private fun updateOneConfigOnce( + nextVersion: Int, + storageClass: ConfigStorageClass, + data: JsonObject + ): JsonObject { + return ConfigFixEvent.publish(ConfigFixEvent(storageClass, nextVersion, data.intoGson().asJsonObject)) + .data.intoKotlinJson().jsonObject + } + + @Subscribe + fun onTick(event: TickEvent) { + val config = configPromise ?: return + val passedTime = saveDebounceStart.passedTime() + if (passedTime < 1.seconds) + return + if (!config.isDone && passedTime < 3.seconds) + return + debugLogger.log("Performing config save") + configPromise = null + saveAll() + } + + val debugLogger = DebugLogger("config") + + var configPromise: CompletableFuture<Void?>? = null + var saveDebounceStart: TimeMark = TimeMark.farPast() + fun markDirty( + holder: IDataHolder<*>, + timeoutPromise: CompletableFuture<Void?>? = null + ) { + debugLogger.log("Config marked dirty") + this.saveDebounceStart = TimeMark.now() + this.configPromise = timeoutPromise ?: CompletableFuture.completedFuture(null) + } + +} diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt new file mode 100644 index 0000000..b92488a --- /dev/null +++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt @@ -0,0 +1,109 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package moe.nea.firmament.gui.config.storage + +import java.nio.file.Path +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.inputStream +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.outputStream +import moe.nea.firmament.Firmament + +// TODO: make this class write / read async +class FirstLevelSplitJsonFolder( + val context: ConfigLoadContext, + val folder: Path +) { + + var hasCreatedBackup = false + + fun backup(cause: String) { + if (hasCreatedBackup) return + hasCreatedBackup = true + context.createBackup(folder, cause) + } + + fun load(): JsonObject { + context.logInfo("Loading FLSJF from $folder") + if (!folder.exists()) + return JsonObject(mapOf()) + return try { + folder.listDirectoryEntries("*.json") + .mapNotNull(::loadIndividualFile) + .toMap() + .let(::JsonObject) + .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") } + } catch (ex: Exception) { + context.logError("Could not load files from $folder", ex) + backup("failed-load") + JsonObject(mapOf()) + } + } + + fun loadIndividualFile(path: Path): Pair<String, JsonElement>? { + context.logDebug("Loading partial file from $path") + return try { + path.inputStream().use { + path.nameWithoutExtension to Firmament.json.decodeFromStream(JsonElement.serializer(), it) + } + } catch (ex: Exception) { + context.logError("Could not load file from $path", ex) + backup("failed-load") + null + } + } + + fun save(value: JsonObject) { + context.logInfo("Saving FLSJF to $folder") + context.logDebug("Current value:\n$value") + if (!folder.exists()) { + context.logInfo("Creating folder $folder") + folder.createDirectories() + } + val entries = folder.listDirectoryEntries("*.json") + .toMutableList() + for ((name, element) in value) { + val path = saveIndividualFile(name, element) + if (path != null) { + entries.remove(path) + } + } + if (entries.isNotEmpty()) { + context.logInfo("Deleting additional files.") + for (path in entries) { + context.logInfo("Deleting $path") + backup("save-deletion") + try { + path.deleteExisting() + } catch (ex: Exception) { + context.logError("Could not delete $path", ex) + } + } + } + context.logInfo("FLSJF to $folder - Voller Erfolg!") + } + + fun saveIndividualFile(name: String, element: JsonElement): Path? { + try { + context.logDebug("Saving partial file with name $name") + val path = folder.resolve("$name.json") + context.ensureWritable(path) + path.outputStream().use { + Firmament.json.encodeToStream(JsonElement.serializer(), element, it) + } + return path + } catch (ex: Exception) { + context.logError("Could not save $name with value $element", ex) + backup("failed-save") + return null + } + } +} diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt new file mode 100644 index 0000000..c1f8b90 --- /dev/null +++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.gui.config.storage + +import java.nio.file.Path +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.moveTo +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.writeText +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configFolder +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configVersionFile +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.storageFolder + +object LegacyImporter { + val legacyConfigVersion = 995 + val backupPath = configFolder.resolveSibling("firmament-legacy-config-${System.currentTimeMillis()}") + + fun copyIf(from: Path, to: Path) { + if (from.exists()) { + to.createParentDirectories() + from.copyTo(to) + } + } + + val legacyStorage = listOf( + "inventory-buttons", + "macros", + ) + + fun importFromLegacy() { + if (!configFolder.exists()) return + configFolder.moveTo(backupPath) + configFolder.createDirectories() + + legacyStorage.forEach { + copyIf( + backupPath.resolve("$it.json"), + storageFolder.resolve("$it.json") + ) + } + + backupPath.listDirectoryEntries("*.json") + .filter { it.nameWithoutExtension !in legacyStorage } + .forEach { path -> + val name = path.name + path.copyTo(configFolder.resolve(name)) + } + + backupPath.resolve("profiles") + .takeIf { it.exists() } + ?.forEachDirectoryEntry { category -> + category.forEachDirectoryEntry { profile -> + copyIf( + profile, + FirmamentConfigLoader.profilePath + .resolve(profile.nameWithoutExtension) + .resolve(category.name + ".json") + ) + } + } + + configVersionFile.writeText("$legacyConfigVersion LEGACY") + } +} diff --git a/src/main/kotlin/gui/config/storage/README.md b/src/main/kotlin/gui/config/storage/README.md new file mode 100644 index 0000000..aad4afe --- /dev/null +++ b/src/main/kotlin/gui/config/storage/README.md @@ -0,0 +1,68 @@ +<!-- +SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe> + +SPDX-License-Identifier: CC0-1.0 +--> + +# Plan for the 2026 Config Renewal of Firmament + +The current config system in Firmament is not growing at a reasonable pace. Here is a list of my grievances with it: + +- the config files are split, resulting in making migrations between different config files (which might load in + different order) difficult +- it is difficult to detect extraneous properties / files, because not all files are loaded and consumed at once +- profile specific data should be in a different hierarchy. the current hierarchy of `profiles/topic/<uuid>.json` orders + data from different profiles to be closer than data from the same profile. this also contributes to the two former + problems. + +## Goals + +- i want to retain having multiple different files for different topics, as well as a folder structure that makes sense + for profiles. +- i want to split up "storage" type data, with "config" type data +- i want to support partial loads with some broken files (resetting the files that are broken) +- i want to support backups on any detected error (or simply at will) + - notably i do not care about the structure of the backups much. even just a all json files merged backup is fine + for me, for now. + +## Implementation + +### FirstLevelSplitJsonFolder + +One of the basic components of this new config folder is a `FirstLevelSplitJsonFolder`. A `FLSJF` takes in a folder +containing multiple JSON-files and loads all of them unconditionally. Each file is then inserted side by side into a +json object, to be processed further by other mechanisms. + +In essence the `FLSJF` takes a folder structure like this: + +``` +file-1.json +file-2.json +file-3.json +``` + +and turns it into a single merged json object: + +```json +{ + "file-1": "the json content of file-1.json", + "file-2": "the json content of file-2.json", + "file-3": "the json content of file-3.json" +} +``` + +As with any stage of the implementation, any unparsable files shall be copied over to a backup spot and discarded. + +Nota bene: Folders are wholesale ignored. + +### Config folders + +Firmament stores all configs and data in the root config folder `./config/firmament`. + +- Any config data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in the root config folder +- Any generic storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in `${rootConfigFolder}/storage/`. +- Any profile specific storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) for each profile in `${rootConfigFolder}/profileStorage/${profileUuid}/`. +- Any backup data is stored in `${rootConfigFolder}/backups/${launchId}/${loadId}/${fileName}`. + - Where `launchId` is `${currentLaunchTimestamp}-${random()}` to avoid collisions. + - Where `loadId` depends on which stage of the config load we are doing (`merge`/`upgrade`/etc.) and what type of config we are loading (`profileSpecific`/`config`/etc.). + - And where `fileName` may be a relative filename of where this data was originally found or some internal descriptor for the merged data stage we are on. diff --git a/src/main/kotlin/gui/entity/EntityModifier.kt b/src/main/kotlin/gui/entity/EntityModifier.kt index 9623070..4915ebb 100644 --- a/src/main/kotlin/gui/entity/EntityModifier.kt +++ b/src/main/kotlin/gui/entity/EntityModifier.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity +import net.minecraft.world.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 index fd7a0c4..4972709 100644 --- a/src/main/kotlin/gui/entity/EntityRenderer.kt +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -3,17 +3,18 @@ package moe.nea.firmament.gui.entity import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonObject +import me.shedaniel.math.Dimension 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 net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.EntitySpawnReason +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.level.Level import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.iterate @@ -21,47 +22,87 @@ import moe.nea.firmament.util.openFirmamentResource import moe.nea.firmament.util.render.enableScissorWithTranslation object EntityRenderer { - val fakeWorld: World get() = MC.lastWorld!! + val fakeWorld: Level get() = MC.lastWorld!! private fun <T : Entity> t(entityType: EntityType<T>): () -> T { - return { entityType.create(fakeWorld, SpawnReason.LOAD)!! } + return { entityType.create(fakeWorld, EntitySpawnReason.LOAD)!! } } val entityIds: Map<String, () -> LivingEntity> = mapOf( - "Zombie" to t(EntityType.ZOMBIE), + "Armadillo" to t(EntityType.ARMADILLO), + "ArmorStand" to t(EntityType.ARMOR_STAND), + "Axolotl" to t(EntityType.AXOLOTL), + "Bat" to t(EntityType.BAT), + "Bee" to t(EntityType.BEE), + "Blaze" to t(EntityType.BLAZE), + "Bogged" to t(EntityType.BOGGED), + "Breeze" to t(EntityType.BREEZE), + "CaveSpider" to t(EntityType.CAVE_SPIDER), "Chicken" to t(EntityType.CHICKEN), - "Slime" to t(EntityType.SLIME), - "Wolf" to t(EntityType.WOLF), - "Skeleton" to t(EntityType.SKELETON), + "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), - "Blaze" to t(EntityType.BLAZE), + "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), + "Salmon" to t(EntityType.SALMON), "Sheep" to t(EntityType.SHEEP), - "Horse" to t(EntityType.HORSE), - "Eisengolem" to t(EntityType.IRON_GOLEM), + "Shulker" to t(EntityType.SHULKER), "Silverfish" to t(EntityType.SILVERFISH), - "Witch" to t(EntityType.WITCH), - "Endermite" to t(EntityType.ENDERMITE), + "Skeleton" to t(EntityType.SKELETON), + "Slime" to t(EntityType.SLIME), + "Sniffer" to t(EntityType.SNIFFER), "Snowman" to t(EntityType.SNOW_GOLEM), - "Villager" to t(EntityType.VILLAGER), - "Guardian" to t(EntityType.GUARDIAN), - "ArmorStand" to t(EntityType.ARMOR_STAND), - "Squid" to t(EntityType.SQUID), - "Bat" to t(EntityType.BAT), "Spider" to t(EntityType.SPIDER), - "CaveSpider" to t(EntityType.CAVE_SPIDER), - "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN), - "Ghast" to t(EntityType.GHAST), - "MagmaCube" to t(EntityType.MAGMA_CUBE), + "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), - "Enderman" to t(EntityType.ENDERMAN), - "Mooshroom" to t(EntityType.MOOSHROOM), "WitherSkeleton" to t(EntityType.WITHER_SKELETON), - "Cow" to t(EntityType.COW), - "Dragon" to t(EntityType.ENDER_DRAGON), - "Player" to { makeGuiPlayer(fakeWorld) }, - "Pig" to t(EntityType.PIG), - "Giant" to t(EntityType.GIANT), + "Wolf" to t(EntityType.WOLF), + "Zoglin" to t(EntityType.ZOGLIN), + "Zombie" to t(EntityType.ZOMBIE), + "ZombieVillager" to t(EntityType.ZOMBIE_VILLAGER) ) val entityModifiers: Map<String, EntityModifier> = mapOf( "playerdata" to ModifyPlayerSkin, @@ -83,7 +124,8 @@ object EntityRenderer { 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 } + "Could not create entity with id $entityId. Failed to apply modifier $modifierJson" + ) { return null } entity = modifier.apply(entity, modifierJson) } return entity @@ -98,7 +140,7 @@ object EntityRenderer { } private val gson = Gson() - fun constructEntity(location: Identifier): LivingEntity? { + fun constructEntity(location: ResourceLocation): LivingEntity? { return constructEntity( gson.fromJson( location.openFirmamentResource().bufferedReader(), JsonObject::class.java @@ -108,7 +150,7 @@ object EntityRenderer { fun renderEntity( entity: LivingEntity, - renderContext: DrawContext, + renderContext: GuiGraphics, posX: Int, posY: Int, // TODO: Add width, height properties here @@ -121,10 +163,10 @@ object EntityRenderer { var bottomOffset = 0.0 var currentEntity = entity val maxSize = entity.iterate { it.firstPassenger as? LivingEntity } - .map { it.height } + .map { it.bbHeight } .sum() while (true) { - currentEntity.age = MC.player?.age ?: 0 + currentEntity.tickCount = MC.player?.tickCount ?: 0 drawEntity( renderContext, posX, @@ -138,14 +180,14 @@ object EntityRenderer { currentEntity ) val next = currentEntity.firstPassenger as? LivingEntity ?: break - bottomOffset += currentEntity.getPassengerRidingPos(next).y.toFloat() * 0.75F + bottomOffset += currentEntity.getPassengerRidingPosition(next).y.toFloat() * 0.75F currentEntity = next } } fun drawEntity( - context: DrawContext, + context: GuiGraphics, x1: Int, y1: Int, x2: Int, @@ -162,38 +204,38 @@ object EntityRenderer { 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 targetPitch = atan(((centerY - mouseY) / hh - entity.eyeHeight * hh / 40)).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.prevHeadYaw - 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.prevHeadYaw = entity.yaw - val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f) - InventoryScreen.drawEntity( + val oldBodyYaw = entity.yBodyRot + val oldYaw = entity.yRot + val oldPitch = entity.xRot + val oldPrevHeadYaw = entity.yHeadRotO + val oldHeadYaw = entity.yHeadRot + entity.yBodyRot = 180.0f + targetYaw * 20.0f + entity.yRot = 180.0f + targetYaw * 40.0f + entity.xRot = -targetPitch * 20.0f + entity.yHeadRot = entity.yRot + entity.yHeadRotO = entity.yRot + val vector3f = Vector3f(0.0f, (entity.bbHeight / 2.0f + bottomOffset).toFloat(), 0.0f) + InventoryScreen.renderEntityInInventory( // TODO: fix multiple entities rendering the same entity context, - centerX, - centerY, + x1, y1, + x2, y2, size.toFloat(), vector3f, rotateToFaceTheFront, rotateToFaceTheCamera, entity ) - entity.bodyYaw = oldBodyYaw - entity.yaw = oldYaw - entity.pitch = oldPitch - entity.prevHeadYaw = oldPrevHeadYaw - entity.headYaw = oldHeadYaw + entity.yBodyRot = oldBodyYaw + entity.yRot = oldYaw + entity.xRot = oldPitch + entity.yHeadRotO = oldPrevHeadYaw + entity.yHeadRot = oldHeadYaw context.disableScissor() } - + val defaultSize = Dimension(50, 80) } diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt deleted file mode 100644 index ccf6b60..0000000 --- a/src/main/kotlin/gui/entity/FakeWorld.kt +++ /dev/null @@ -1,343 +0,0 @@ -package moe.nea.firmament.gui.entity - -import java.util.UUID -import java.util.function.BooleanSupplier -import java.util.function.Consumer -import net.minecraft.block.Block -import net.minecraft.block.BlockState -import net.minecraft.client.gui.screen.world.SelectWorldScreen -import net.minecraft.component.type.MapIdComponent -import net.minecraft.entity.Entity -import net.minecraft.entity.boss.dragon.EnderDragonPart -import net.minecraft.entity.damage.DamageSource -import net.minecraft.entity.player.PlayerEntity -import net.minecraft.fluid.Fluid -import net.minecraft.item.FuelRegistry -import net.minecraft.item.map.MapState -import net.minecraft.particle.ParticleEffect -import net.minecraft.recipe.BrewingRecipeRegistry -import net.minecraft.recipe.RecipeManager -import net.minecraft.recipe.RecipePropertySet -import net.minecraft.recipe.StonecuttingRecipe -import net.minecraft.recipe.display.CuttingRecipeDisplay -import net.minecraft.registry.DynamicRegistryManager -import net.minecraft.registry.Registries -import net.minecraft.registry.RegistryKey -import net.minecraft.registry.RegistryKeys -import net.minecraft.registry.ServerDynamicRegistryType -import net.minecraft.registry.entry.RegistryEntry -import net.minecraft.resource.DataConfiguration -import net.minecraft.resource.ResourcePackManager -import net.minecraft.resource.featuretoggle.FeatureFlags -import net.minecraft.resource.featuretoggle.FeatureSet -import net.minecraft.scoreboard.Scoreboard -import net.minecraft.server.SaveLoading -import net.minecraft.server.command.CommandManager -import net.minecraft.sound.SoundCategory -import net.minecraft.sound.SoundEvent -import net.minecraft.util.Identifier -import net.minecraft.util.TypeFilter -import net.minecraft.util.function.LazyIterationConsumer -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box -import net.minecraft.util.math.ChunkPos -import net.minecraft.util.math.Direction -import net.minecraft.util.math.Vec3d -import net.minecraft.world.BlockView -import net.minecraft.world.Difficulty -import net.minecraft.world.MutableWorldProperties -import net.minecraft.world.World -import net.minecraft.world.biome.Biome -import net.minecraft.world.biome.BiomeKeys -import net.minecraft.world.chunk.Chunk -import net.minecraft.world.chunk.ChunkManager -import net.minecraft.world.chunk.ChunkStatus -import net.minecraft.world.chunk.EmptyChunk -import net.minecraft.world.chunk.light.LightingProvider -import net.minecraft.world.entity.EntityLookup -import net.minecraft.world.event.GameEvent -import net.minecraft.world.explosion.ExplosionBehavior -import net.minecraft.world.tick.OrderedTick -import net.minecraft.world.tick.QueryableTickScheduler -import net.minecraft.world.tick.TickManager -import moe.nea.firmament.util.MC - -fun createDynamicRegistry(): DynamicRegistryManager.Immutable { - // TODO: use SaveLoading.load() to properly load a full registry - return DynamicRegistryManager.of(Registries.REGISTRIES) -} - -class FakeWorld( - registries: DynamicRegistryManager.Immutable = createDynamicRegistry(), -) : World( - Properties, - RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")), - registries, - MC.defaultRegistries.getOrThrow(RegistryKeys.DIMENSION_TYPE) - .getOrThrow(RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("minecraft", "overworld"))), - true, - false, - 0L, - 0 -) { - object Properties : MutableWorldProperties { - override fun getSpawnPos(): BlockPos { - return BlockPos.ORIGIN - } - - override fun getSpawnAngle(): Float { - return 0F - } - - override fun getTime(): Long { - return 0 - } - - override fun getTimeOfDay(): Long { - return 0 - } - - override fun isThundering(): Boolean { - return false - } - - override fun isRaining(): Boolean { - return false - } - - override fun setRaining(raining: Boolean) { - } - - override fun isHardcore(): Boolean { - return false - } - - override fun getDifficulty(): Difficulty { - return Difficulty.HARD - } - - override fun isDifficultyLocked(): Boolean { - return false - } - - override fun setSpawnPos(pos: BlockPos?, angle: Float) {} - } - - override fun getPlayers(): List<PlayerEntity> { - return emptyList() - } - - override fun getBrightness(direction: Direction?, shaded: Boolean): Float { - return 1f - } - - override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> { - return registryManager.getOptionalEntry(BiomeKeys.PLAINS).get() - } - - override fun getSeaLevel(): Int { - return 0 - } - - override fun getEnabledFeatures(): FeatureSet { - return FeatureFlags.VANILLA_FEATURES - } - - class FakeTickScheduler<T> : QueryableTickScheduler<T> { - override fun scheduleTick(orderedTick: OrderedTick<T>?) { - } - - override fun isQueued(pos: BlockPos?, type: T): Boolean { - return true - } - - override fun getTickCount(): Int { - return 0 - } - - override fun isTicking(pos: BlockPos?, type: T): Boolean { - return true - } - - } - - override fun getBlockTickScheduler(): QueryableTickScheduler<Block> { - return FakeTickScheduler() - } - - override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> { - return FakeTickScheduler() - } - - - class FakeChunkManager(val world: FakeWorld) : ChunkManager() { - override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk { - return EmptyChunk( - world, - ChunkPos(x, z), - world.registryManager.getOptionalEntry(BiomeKeys.PLAINS).get() - ) - } - - override fun getWorld(): BlockView { - return world - } - - override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) { - } - - override fun getDebugString(): String { - return "FakeChunkManager" - } - - override fun getLoadedChunkCount(): Int { - return 0 - } - - override fun getLightingProvider(): LightingProvider { - return FakeLightingProvider(this) - } - } - - class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false) - - override fun getChunkManager(): ChunkManager { - return FakeChunkManager(this) - } - - override fun playSound( - source: PlayerEntity?, - x: Double, - y: Double, - z: Double, - sound: RegistryEntry<SoundEvent>?, - category: SoundCategory?, - volume: Float, - pitch: Float, - seed: Long - ) { - } - - override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) { - } - - override fun emitGameEvent(event: RegistryEntry<GameEvent>?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) { - } - - override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) { - } - - override fun playSoundFromEntity( - source: PlayerEntity?, - entity: Entity?, - sound: RegistryEntry<SoundEvent>?, - category: SoundCategory?, - volume: Float, - pitch: Float, - seed: Long - ) { - } - - override fun createExplosion( - entity: Entity?, - damageSource: DamageSource?, - behavior: ExplosionBehavior?, - x: Double, - y: Double, - z: Double, - power: Float, - createFire: Boolean, - explosionSourceType: ExplosionSourceType?, - smallParticle: ParticleEffect?, - largeParticle: ParticleEffect?, - soundEvent: RegistryEntry<SoundEvent>? - ) { - TODO("Not yet implemented") - } - - override fun asString(): String { - return "FakeWorld" - } - - override fun getEntityById(id: Int): Entity? { - return null - } - - override fun getEnderDragonParts(): MutableCollection<EnderDragonPart> { - return mutableListOf() - } - - override fun getTickManager(): TickManager { - return TickManager() - } - - override fun getMapState(id: MapIdComponent?): MapState? { - return null - } - - override fun putMapState(id: MapIdComponent?, state: MapState?) { - } - - override fun increaseAndGetMapId(): MapIdComponent { - return MapIdComponent(0) - } - - override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) { - } - - override fun getScoreboard(): Scoreboard { - return Scoreboard() - } - - override fun getRecipeManager(): RecipeManager { - return object : RecipeManager { - override fun getPropertySet(key: RegistryKey<RecipePropertySet>?): RecipePropertySet { - return RecipePropertySet.EMPTY - } - - override fun getStonecutterRecipes(): CuttingRecipeDisplay.Grouping<StonecuttingRecipe> { - return CuttingRecipeDisplay.Grouping.empty() - } - } - } - - object FakeEntityLookup : EntityLookup<Entity> { - override fun get(id: Int): Entity? { - return null - } - - override fun get(uuid: UUID?): Entity? { - return null - } - - override fun iterate(): MutableIterable<Entity> { - return mutableListOf() - } - - override fun <U : Entity?> forEachIntersects( - filter: TypeFilter<Entity, U>?, - box: Box?, - consumer: LazyIterationConsumer<U>? - ) { - } - - override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) { - } - - override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) { - } - - } - - override fun getEntityLookup(): EntityLookup<Entity> { - return FakeEntityLookup - } - - override fun getBrewingRecipeRegistry(): BrewingRecipeRegistry { - return BrewingRecipeRegistry.EMPTY - } - - override fun getFuelRegistry(): FuelRegistry { - TODO("Not yet implemented") - } -} diff --git a/src/main/kotlin/gui/entity/GuiPlayer.kt b/src/main/kotlin/gui/entity/GuiPlayer.kt index f728dbf..b53f68c 100644 --- a/src/main/kotlin/gui/entity/GuiPlayer.kt +++ b/src/main/kotlin/gui/entity/GuiPlayer.kt @@ -1,62 +1,28 @@ 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 +import net.minecraft.client.entity.ClientMannequin +import net.minecraft.client.resources.DefaultPlayerSkin +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.player.PlayerSkin +import net.minecraft.world.level.Level +import moe.nea.firmament.util.MC -/** - * @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() +fun makeGuiPlayer(world: Level): GuiPlayer { + val player = GuiPlayer(MC.instance.level!!) return player } -class GuiPlayer(world: ClientWorld?, profile: GameProfile?) : AbstractClientPlayerEntity(world, profile) { +class GuiPlayer(world: ClientLevel?) : ClientMannequin(world, MC.instance.playerSkinRenderCache()) { 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 { + override fun shouldShowName(): 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 - ) + var skinTextures: PlayerSkin = DefaultPlayerSkin.get(this.uuid) // TODO: 1.21.10 + override fun getSkin(): PlayerSkin { + return skinTextures } } diff --git a/src/main/kotlin/gui/entity/ModifyAge.kt b/src/main/kotlin/gui/entity/ModifyAge.kt index a65c368..99154ef 100644 --- a/src/main/kotlin/gui/entity/ModifyAge.kt +++ b/src/main/kotlin/gui/entity/ModifyAge.kt @@ -2,19 +2,19 @@ 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 +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.entity.monster.Zombie +import net.minecraft.world.entity.AgeableMob 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) { + if (entity is AgeableMob) { + entity.age = if (isBaby) -1 else 1 + } else if (entity is Zombie) { entity.isBaby = isBaby - } else if (entity is ArmorStandEntity) { + } else if (entity is ArmorStand) { entity.isSmall = isBaby } else { error("Cannot set age for $entity") diff --git a/src/main/kotlin/gui/entity/ModifyCharged.kt b/src/main/kotlin/gui/entity/ModifyCharged.kt index d22f6e3..23fd495 100644 --- a/src/main/kotlin/gui/entity/ModifyCharged.kt +++ b/src/main/kotlin/gui/entity/ModifyCharged.kt @@ -2,13 +2,13 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity -import net.minecraft.entity.mob.CreeperEntity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.monster.Creeper object ModifyCharged : EntityModifier { override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { - require(entity is CreeperEntity) - entity.dataTracker.set(CreeperEntity.CHARGED, true) + require(entity is Creeper) + entity.entityData.set(Creeper.DATA_IS_POWERED, true) return entity } } diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt index a558936..9c43e73 100644 --- a/src/main/kotlin/gui/entity/ModifyEquipment.kt +++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt @@ -1,17 +1,18 @@ 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 net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.DyedItemColor +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.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.arbitraryUUID import moe.nea.firmament.util.mc.setEncodedSkullOwner -import moe.nea.firmament.util.mc.zeroUUID object ModifyEquipment : EntityModifier { val names = mapOf( @@ -25,18 +26,19 @@ object ModifyEquipment : EntityModifier { override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { names.forEach { (key, slot) -> info[key]?.let { - entity.equipStack(slot, createItem(it.asString)) + entity.setItemSlot(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(zeroUUID, data) } + "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) @@ -47,7 +49,7 @@ object ModifyEquipment : EntityModifier { private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack { val stack = ItemStack(leatherArmor) - stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16), false)) + stack.set(DataComponents.DYED_COLOR, DyedItemColor(data.toInt(16))) return stack } } diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt index f094ca4..a870bf1 100644 --- a/src/main/kotlin/gui/entity/ModifyHorse.kt +++ b/src/main/kotlin/gui/entity/ModifyHorse.kt @@ -1,62 +1,58 @@ - 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.LivingEntity -import net.minecraft.entity.SpawnReason -import net.minecraft.entity.passive.AbstractHorseEntity -import net.minecraft.item.ItemStack -import net.minecraft.item.Items +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.EntitySpawnReason +import net.minecraft.world.entity.animal.horse.AbstractHorse +import net.minecraft.world.item.ItemStack +import net.minecraft.world.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 - } + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is AbstractHorse) + var entity: AbstractHorse = entity + info["kind"]?.let { + entity = when (it.asString) { + "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, EntitySpawnReason.LOAD)!! + "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, EntitySpawnReason.LOAD)!! + "mule" -> EntityType.MULE.create(fakeWorld, EntitySpawnReason.LOAD)!! + "donkey" -> EntityType.DONKEY.create(fakeWorld, EntitySpawnReason.LOAD)!! + "horse" -> EntityType.HORSE.create(fakeWorld, EntitySpawnReason.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) { - val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS) - dataTracker.set( - AbstractHorseEntity.HORSE_FLAGS, - if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte() - else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv() - ) +fun AbstractHorse.setIsSaddled(shouldBeSaddled: Boolean) { + this.setItemSlot( + EquipmentSlot.SADDLE, + if (shouldBeSaddled) ItemStack(Items.SADDLE) + else ItemStack.EMPTY + ) } -fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) { - items.setStack(1, itemStack) +fun AbstractHorse.setHorseArmor(itemStack: ItemStack) { + bodyArmorItem = itemStack } diff --git a/src/main/kotlin/gui/entity/ModifyInvisible.kt b/src/main/kotlin/gui/entity/ModifyInvisible.kt index 8d36991..7a54ab1 100644 --- a/src/main/kotlin/gui/entity/ModifyInvisible.kt +++ b/src/main/kotlin/gui/entity/ModifyInvisible.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity +import net.minecraft.world.entity.LivingEntity object ModifyInvisible : EntityModifier { override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { diff --git a/src/main/kotlin/gui/entity/ModifyName.kt b/src/main/kotlin/gui/entity/ModifyName.kt index a03da96..6def0b4 100644 --- a/src/main/kotlin/gui/entity/ModifyName.kt +++ b/src/main/kotlin/gui/entity/ModifyName.kt @@ -2,12 +2,12 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity -import net.minecraft.text.Text +import net.minecraft.world.entity.LivingEntity +import net.minecraft.network.chat.Component object ModifyName : EntityModifier { override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { - entity.customName = Text.literal(info.get("name").asString) + entity.customName = Component.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 index 28f0070..0b393bb 100644 --- a/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt +++ b/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt @@ -1,47 +1,57 @@ - 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 +import net.minecraft.client.entity.ClientAvatarEntity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.Avatar +import net.minecraft.world.entity.player.Player +import net.minecraft.world.entity.player.PlayerModelPart +import net.minecraft.world.entity.player.PlayerModelType +import net.minecraft.world.entity.player.PlayerSkin +import net.minecraft.core.ClientAsset +import net.minecraft.resources.ResourceLocation 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 - } + val playerModelPartIndex = PlayerModelPart.entries.associateBy { it.id } + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is GuiPlayer) + var capeTexture = entity.skinTextures.cape + var model = entity.skinTextures.model + var bodyTexture = entity.skinTextures.body + fun mkTexAsset(id: ResourceLocation) = ClientAsset.ResourceTexture(id, id) + info["cape"]?.let { + capeTexture = mkTexAsset(ResourceLocation.parse(it.asString)) + } + info["skin"]?.let { + bodyTexture = mkTexAsset(ResourceLocation.parse(it.asString)) + } + info["slim"]?.let { + model = if (it.asBoolean) PlayerModelType.SLIM else PlayerModelType.WIDE + } + info["parts"]?.let { + var trackedData = entity.entityData.get(Avatar.DATA_PLAYER_MODE_CUSTOMISATION) + 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.mask.inv().toByte()) + } else { + trackedData or (part.mask.toByte()) + } + } + } + entity.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, trackedData) + } + entity.skinTextures = PlayerSkin( + bodyTexture, capeTexture, null, model, true + ) + return entity + } } diff --git a/src/main/kotlin/gui/entity/ModifyRiding.kt b/src/main/kotlin/gui/entity/ModifyRiding.kt index 5c4c78d..51fcfae 100644 --- a/src/main/kotlin/gui/entity/ModifyRiding.kt +++ b/src/main/kotlin/gui/entity/ModifyRiding.kt @@ -2,13 +2,13 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity +import net.minecraft.world.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) + newEntity.startRiding(entity, true, false) return entity } diff --git a/src/main/kotlin/gui/entity/ModifyWither.kt b/src/main/kotlin/gui/entity/ModifyWither.kt index 6083d88..67252b8 100644 --- a/src/main/kotlin/gui/entity/ModifyWither.kt +++ b/src/main/kotlin/gui/entity/ModifyWither.kt @@ -2,14 +2,14 @@ package moe.nea.firmament.gui.entity import com.google.gson.JsonObject -import net.minecraft.entity.LivingEntity -import net.minecraft.entity.boss.WitherEntity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.boss.wither.WitherBoss object ModifyWither : EntityModifier { override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { - require(entity is WitherEntity) + require(entity is WitherBoss) info["tiny"]?.let { - entity.setInvulTimer(if (it.asBoolean) 800 else 0) + entity.invulnerableTicks = if (it.asBoolean) 800 else 0 } info["armored"]?.let { entity.health = if (it.asBoolean) 1F else entity.maxHealth diff --git a/src/main/kotlin/gui/hud/MoulConfigHud.kt b/src/main/kotlin/gui/hud/MoulConfigHud.kt index e99b069..b5d7cf7 100644 --- a/src/main/kotlin/gui/hud/MoulConfigHud.kt +++ b/src/main/kotlin/gui/hud/MoulConfigHud.kt @@ -1,66 +1,68 @@ - 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.ResourceManagerReloadListener +import net.minecraft.network.chat.Component import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.jarvis.JarvisIntegration import moe.nea.firmament.util.MC import moe.nea.firmament.util.MoulConfigUtils abstract class MoulConfigHud( - val name: String, - val hudMeta: HudMeta, + val name: String, + val hudMeta: HudMeta, ) { - companion object { - private val componentWrapper by lazy { - object : GuiComponentWrapper(GuiContext(TextComponent("§cERROR"))) { - init { - this.client = MC.instance - } - } - } - } + companion object { + private val componentWrapper by lazy { + object : MoulConfigScreenComponent(Component.empty(), GuiContext(TextComponent("§cERROR")), null) { + init { + this.minecraft = MC.instance + } + } + } + } - private var fragment: GuiContext? = null + private var fragment: GuiContext? = null - fun forceInit() { - } + fun forceInit() { + } - open fun shouldRender(): Boolean { - return true - } + 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 - } - }) - } - } + 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.pose().pushMatrix() + hudMeta.applyTransformations(it.context.pose()) + val pos = hudMeta.getEffectivePosition(JarvisIntegration.jarvis) + val renderContextTranslated = + renderContext.translated(pos.x(), pos.y(), hudMeta.effectiveWidth, hudMeta.effectiveHeight) + .scaled(hudMeta.scale) + fragment!!.root.render(renderContextTranslated) + it.context.pose().popMatrix() + } + FinalizeResourceManagerEvent.subscribe("MoulConfigHud:finalizeResourceManager") { + MC.resourceManager.registerReloadListener(object : ResourceManagerReloadListener { + override fun onResourceManagerReload(manager: ResourceManager?) { + fragment = null + } + }) + } + } - fun loadFragment() { - fragment = MoulConfigUtils.loadGui(name, this) - } + fun loadFragment() { + fragment = MoulConfigUtils.loadGui(name, this) + } } diff --git a/src/main/kotlin/impl/v1/FirmamentAPIImpl.kt b/src/main/kotlin/impl/v1/FirmamentAPIImpl.kt new file mode 100644 index 0000000..8c6b732 --- /dev/null +++ b/src/main/kotlin/impl/v1/FirmamentAPIImpl.kt @@ -0,0 +1,67 @@ +package moe.nea.firmament.impl.v1 + +import java.util.Collections +import java.util.Optional +import net.fabricmc.loader.api.FabricLoader +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.item.ItemStack +import moe.nea.firmament.Firmament +import moe.nea.firmament.api.v1.FirmamentAPI +import moe.nea.firmament.api.v1.FirmamentExtension +import moe.nea.firmament.api.v1.FirmamentItemWidget +import moe.nea.firmament.features.items.recipes.ItemList +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.intoOptional + +object FirmamentAPIImpl : FirmamentAPI() { + @JvmField + val INSTANCE: FirmamentAPI = FirmamentAPIImpl + + private val _extensions = mutableListOf<FirmamentExtension>() + override fun getExtensions(): List<FirmamentExtension> { + return Collections.unmodifiableList(_extensions) + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun getHoveredItemWidget(): Optional<FirmamentItemWidget> { + val mouse = MC.instance.mouseHandler + val window = MC.window + val xpos = mouse.getScaledXPos(window) + val ypos = mouse.getScaledYPos(window) + val widget = MC.screen + ?.getChildAt(xpos, ypos) + ?.getOrNull() + if (widget is FirmamentItemWidget) return widget.intoOptional() + val itemListStack = ItemList.findStackUnder(xpos.toInt(), ypos.toInt()) + if (itemListStack != null) + return object : FirmamentItemWidget { + override fun getPlacement(): FirmamentItemWidget.Placement { + return FirmamentItemWidget.Placement.ITEM_LIST + } + + override fun getItemStack(): ItemStack { + return itemListStack.second.asImmutableItemStack() + } + + override fun getSkyBlockId(): String { + return itemListStack.second.skyblockId.neuItem + } + + }.intoOptional() + return Optional.empty() + } + + fun loadExtensions() { + for (container in FabricLoader.getInstance() + .getEntrypointContainers(FirmamentExtension.ENTRYPOINT_NAME, FirmamentExtension::class.java)) { + Firmament.logger.info("Loading extension ${container.entrypoint} from ${container.provider.metadata.name}") + loadExtension(container.entrypoint) + } + extensions.forEach { it.onLoad() } + } + + fun loadExtension(entrypoint: FirmamentExtension) { + _extensions.add(entrypoint) + } +} diff --git a/src/main/kotlin/jarvis/JarvisIntegration.kt b/src/main/kotlin/jarvis/JarvisIntegration.kt index 96f47f7..58c606b 100644 --- a/src/main/kotlin/jarvis/JarvisIntegration.kt +++ b/src/main/kotlin/jarvis/JarvisIntegration.kt @@ -6,13 +6,13 @@ 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 net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component 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 +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader +import moe.nea.firmament.util.data.ManagedConfig class JarvisIntegration : JarvisPlugin { override fun getModId(): String = @@ -27,9 +27,7 @@ class JarvisIntegration : JarvisPlugin { } val configs - get() = listOf( - RepoManager.Config - ) + FeatureManager.allFeatures.mapNotNull { it.config } + get() = FirmamentConfigLoader.allConfigs.filterIsInstance<ManagedConfig>() override fun getAllHuds(): List<JarvisHud> { @@ -39,18 +37,18 @@ class JarvisIntegration : JarvisPlugin { } override fun onHudEditorClosed() { - configs.forEach { it.save() } + configs.forEach { it.markDirty() } } override fun getAllConfigOptions(): List<JarvisConfigOption> { return configs.flatMap { config -> config.sortedOptions.map { object : JarvisConfigOption { - override fun title(): Text { + override fun title(): Component { return it.labelText } - override fun description(): List<Text> { + override fun description(): List<Component> { return emptyList() } diff --git a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt index 59b131a..63b7232 100644 --- a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt +++ b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt @@ -1,18 +1,23 @@ 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 net.minecraft.client.KeyMapping +import com.mojang.blaze3d.platform.InputConstants +import moe.nea.firmament.Firmament import moe.nea.firmament.gui.config.ManagedOption import moe.nea.firmament.util.TestUtil +import moe.nea.firmament.util.data.ManagedConfig object FirmamentKeyBindings { + val cats = mutableMapOf<ManagedConfig.Category, KeyMapping.Category>() + + fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) { - val vanillaKeyBinding = KeyBinding( + val vanillaKeyBinding = KeyMapping( name, - InputUtil.Type.KEYSYM, + InputConstants.Type.KEYSYM, -1, - "firmament.key.category" + cats.computeIfAbsent(config.element.category) { KeyMapping.Category(Firmament.identifier(it.name.lowercase())) } ) if (!TestUtil.isInTest) { KeyBindingHelper.registerKeyBinding(vanillaKeyBinding) @@ -20,6 +25,6 @@ object FirmamentKeyBindings { keyBindings[vanillaKeyBinding] = config } - val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>() + val keyBindings = mutableMapOf<KeyMapping, ManagedOption<SavedKeyBinding>>() } diff --git a/src/main/kotlin/keybindings/FirmamentKeyboardState.kt b/src/main/kotlin/keybindings/FirmamentKeyboardState.kt new file mode 100644 index 0000000..da94a44 --- /dev/null +++ b/src/main/kotlin/keybindings/FirmamentKeyboardState.kt @@ -0,0 +1,24 @@ +package moe.nea.firmament.keybindings + +import java.util.BitSet +import org.lwjgl.glfw.GLFW +import net.minecraft.client.input.KeyEvent + +object FirmamentKeyboardState { + + private val pressedScancodes = BitSet() + + @Synchronized + fun isScancodeDown(scancode: Int): Boolean { + // TODO: maintain a record of keycodes that were pressed for this scanCode to check if they are still held + return pressedScancodes.get(scancode) + } + + @Synchronized + fun maintainState(keyInput: KeyEvent, action: Int) { + when (action) { + GLFW.GLFW_PRESS -> pressedScancodes.set(keyInput.scancode) + GLFW.GLFW_RELEASE -> pressedScancodes.clear(keyInput.scancode) + } + } +} diff --git a/src/main/kotlin/keybindings/GenericInputButton.kt b/src/main/kotlin/keybindings/GenericInputButton.kt new file mode 100644 index 0000000..4a4aaba --- /dev/null +++ b/src/main/kotlin/keybindings/GenericInputButton.kt @@ -0,0 +1,332 @@ +package moe.nea.firmament.keybindings + +import org.lwjgl.glfw.GLFW +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.put +import net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.input.InputWithModifiers +import net.minecraft.client.input.KeyEvent +import net.minecraft.client.input.MouseButtonInfo +import com.mojang.blaze3d.platform.InputConstants +import com.mojang.blaze3d.platform.MacosUtil +import net.minecraft.network.chat.Component +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.InitLevel + +@Serializable(with = GenericInputButton.Serializer::class) +sealed interface GenericInputButton { + + object Serializer : KSerializer<GenericInputButton> { + override val descriptor: SerialDescriptor + get() = SerialDescriptor("Firmament:GenericInputButton", JsonElement.serializer().descriptor) + + override fun serialize( + encoder: Encoder, + value: GenericInputButton + ) { + JsonElement.serializer().serialize( + encoder, + when (value) { + is KeyCodeButton -> buildJsonObject { put("keyCode", value.keyCode) } + is MouseButton -> buildJsonObject { put("mouse", value.mouseButton) } + is ScanCodeButton -> buildJsonObject { put("scanCode", value.scanCode) } + Unbound -> JsonNull + }) + } + + override fun deserialize(decoder: Decoder): GenericInputButton { + val element = JsonElement.serializer().deserialize(decoder) + if (element is JsonNull) + return Unbound + require(element is JsonObject) + (element["keyCode"] as? JsonPrimitive)?.let { + return KeyCodeButton(it.int) + } + (element["mouse"] as? JsonPrimitive)?.let { + return MouseButton(it.int) + } + (element["scanCode"] as? JsonPrimitive)?.let { + return ScanCodeButton(it.int) + } + error("Could not parse GenericInputButton: $element") + } + } + + companion object { + + fun of(event: KeyEvent) = ofKeyAndScan(event.input(), event.scancode) + fun escape() = ofKeyCode(GLFW.GLFW_KEY_ESCAPE) + fun ofKeyCode(keyCode: Int): GenericInputButton = KeyCodeButton(keyCode) + fun ofScanCode(scanCode: Int): GenericInputButton = ScanCodeButton(scanCode) + fun ofScanCodeFromKeyCode(keyCode: Int): GenericInputButton = ScanCodeButton(GLFW.glfwGetKeyScancode(keyCode)) + fun unbound(): GenericInputButton = Unbound + fun mouse(mouseButton: Int): GenericInputButton = MouseButton(mouseButton) + fun ofKeyAndScan(keyCode: Int, scanCode: Int): GenericInputButton { + if (keyCode == GLFW.GLFW_KEY_UNKNOWN) + return ofScanCode(scanCode) + return ofKeyCode(keyCode) // TODO: should i always upgrade to a scanCode? + } + } + + data object Unbound : GenericInputButton { + override fun toInputKey(): InputConstants.Key { + return InputConstants.UNKNOWN + } + + override fun isBound(): Boolean { + return false + } + + override fun isPressed(): Boolean { + return false + } + } + + data class MouseButton( + val mouseButton: Int + ) : GenericInputButton { + override fun toInputKey(): InputConstants.Key { + return InputConstants.Type.MOUSE.getOrCreate(mouseButton) + } + + override fun isPressed(): Boolean { + return GLFW.glfwGetMouseButton(MC.window.handle(), mouseButton) == GLFW.GLFW_PRESS + } + } + + data class KeyCodeButton( + val keyCode: Int + ) : GenericInputButton { + override fun toInputKey(): InputConstants.Key { + return InputConstants.Type.KEYSYM.getOrCreate(keyCode) + } + + override fun isPressed(): Boolean { + return InputConstants.isKeyDown(MC.window, keyCode) + } + + override fun isCtrl(): Boolean { + return keyCode in InputModifiers.controlKeys + } + + override fun isAlt(): Boolean { + return keyCode in InputModifiers.altKeys + } + + override fun isShift(): Boolean { + return keyCode in InputModifiers.shiftKeys + } + + override fun isSuper(): Boolean { + return keyCode in InputModifiers.superKeys + } + } + + data class ScanCodeButton( + val scanCode: Int + ) : GenericInputButton { + override fun toInputKey(): InputConstants.Key { + return InputConstants.Type.SCANCODE.getOrCreate(scanCode) + } + + override fun isPressed(): Boolean { + return FirmamentKeyboardState.isScancodeDown(scanCode) + } + } + + fun isBound() = true + + fun isModifier() = isCtrl() || isAlt() || isSuper() || isShift() + fun isCtrl() = false + fun isAlt() = false + fun isSuper() = false + fun isShift() = false + + fun toInputKey(): InputConstants.Key + fun format(): Component = + if (InitLevel.isAtLeast(InitLevel.RENDER_INIT)) { + toInputKey().displayName + } else { + Component.nullToEmpty(toString()) + } + + fun matches(inputAction: GenericInputAction) = inputAction.matches(this) + fun isPressed(): Boolean +} + +sealed interface GenericInputAction { + fun matches(inputButton: GenericInputButton): Boolean + + data class MouseInput( + val mouseButton: Int + ) : GenericInputAction { + override fun matches(inputButton: GenericInputButton): Boolean { + return inputButton is GenericInputButton.MouseButton && inputButton.mouseButton == mouseButton + } + } + + data class KeyboardInput( + val keyCode: Int, + val scanCode: Int, + ) : GenericInputAction { + override fun matches(inputButton: GenericInputButton): Boolean { + return when (inputButton) { + is GenericInputButton.KeyCodeButton -> inputButton.keyCode == keyCode + is GenericInputButton.ScanCodeButton -> inputButton.scanCode == scanCode + else -> false + } + } + } + + companion object { + @JvmStatic + fun mouse(mouseButton: Int): GenericInputAction = MouseInput(mouseButton) + + @JvmStatic + fun mouse(click: MouseButtonEvent): GenericInputAction = mouse(click.button()) + + @JvmStatic + fun of(input: net.minecraft.client.input.MouseButtonInfo): GenericInputAction = mouse(input.button) + @JvmStatic + fun of(input: KeyEvent): GenericInputAction = key(input.input(), input.scancode) + + @JvmStatic + fun key(keyCode: Int, scanCode: Int): GenericInputAction = KeyboardInput(keyCode, scanCode) + } +} + +@Serializable +data class InputModifiers( + val modifiers: Int +) { + companion object { + @JvmStatic + fun current(): InputModifiers { + val h = MC.window + val ctrl = if (MacosUtil.IS_MACOS) { + InputConstants.isKeyDown(h, GLFW.GLFW_KEY_LEFT_SUPER) + || InputConstants.isKeyDown(h, GLFW.GLFW_KEY_RIGHT_SUPER) + } else InputConstants.isKeyDown(h, GLFW.GLFW_KEY_LEFT_CONTROL) + || InputConstants.isKeyDown(h, GLFW.GLFW_KEY_RIGHT_CONTROL) + val shift = InputConstants.isKeyDown(h, GLFW.GLFW_KEY_LEFT_SHIFT) || InputConstants.isKeyDown( + h, + GLFW.GLFW_KEY_RIGHT_SHIFT + ) + val alt = InputConstants.isKeyDown(h, GLFW.GLFW_KEY_LEFT_ALT) + || InputConstants.isKeyDown(h, GLFW.GLFW_KEY_RIGHT_ALT) + val `super` = InputConstants.isKeyDown(h, GLFW.GLFW_KEY_LEFT_SUPER) + || InputConstants.isKeyDown(h, GLFW.GLFW_KEY_RIGHT_SUPER) + return of( + ctrl = ctrl, + shift = shift, + alt = alt, + `super` = `super`, + ) + } + + + val superKeys = listOf(GLFW.GLFW_KEY_LEFT_SUPER, GLFW.GLFW_KEY_RIGHT_SUPER) + val controlKeys = if (MacosUtil.IS_MACOS) { + 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 of( + vararg useNamedArgs: Boolean, + ctrl: Boolean = false, + shift: Boolean = false, + alt: Boolean = false, + `super`: Boolean = false + ): InputModifiers { + require(useNamedArgs.isEmpty()) + return InputModifiers( + (if (ctrl) GLFW.GLFW_MOD_CONTROL else 0) + or (if (shift) GLFW.GLFW_MOD_SHIFT else 0) + or (if (alt) GLFW.GLFW_MOD_ALT else 0) + or (if (`super`) GLFW.GLFW_MOD_SUPER else 0) + ) + } + + fun ofKeyCodes(vararg keys: Int): InputModifiers { + var mods = 0 + for (key in keys) { + if (key in superKeys) + mods = mods or GLFW.GLFW_MOD_SUPER + if (key in controlKeys) + mods = mods or GLFW.GLFW_MOD_CONTROL + if (key in altKeys) + mods = mods or GLFW.GLFW_MOD_ALT + if (key in shiftKeys) + mods = mods or GLFW.GLFW_MOD_SHIFT + } + return of(mods) + } + + @JvmStatic + fun of(modifiers: Int) = InputModifiers(modifiers) + + @JvmStatic + fun of(input: InputWithModifiers) = InputModifiers(input.modifiers()) + + fun none(): InputModifiers { + return InputModifiers(0) + } + + fun ofKey(button: GenericInputButton): InputModifiers { + return when (button) { + is GenericInputButton.KeyCodeButton -> ofKeyCodes(button.keyCode) + else -> none() + } + } + } + + fun isAtLeast(other: InputModifiers): Boolean { + return this.modifiers and other.modifiers == this.modifiers + } + + fun without(other: InputModifiers): InputModifiers { + return InputModifiers(this.modifiers and other.modifiers.inv()) + } + + fun isEmpty() = modifiers == 0 + + fun getFlag(flag: Int) = modifiers and flag != 0 + val ctrl get() = getFlag(GLFW.GLFW_MOD_CONTROL) // TODO: consult someone on control vs command again + val shift get() = getFlag(GLFW.GLFW_MOD_SHIFT) + val alt get() = getFlag(GLFW.GLFW_MOD_ALT) + val `super` get() = getFlag(GLFW.GLFW_MOD_SUPER) + + override fun toString(): String { + return listOfNotNull( + if (ctrl) "CTRL" else null, + if (shift) "SHIFT" else null, + if (alt) "ALT" else null, + if (`super`) "SUPER" else null, + ).joinToString(" + ") + } + + fun matches(other: InputModifiers, atLeast: Boolean): Boolean { + if (atLeast) + return isAtLeast(other) + return this == other + } + + fun format(): Component { // TODO: translation for mods + return Component.nullToEmpty(toString()) + } + +} diff --git a/src/main/kotlin/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt deleted file mode 100644 index 1975361..0000000 --- a/src/main/kotlin/keybindings/IKeyBinding.kt +++ /dev/null @@ -1,29 +0,0 @@ - - -package moe.nea.firmament.keybindings - -import net.minecraft.client.option.KeyBinding - -interface IKeyBinding { - fun matches(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.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers - } - } - } - - companion object { - fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding { - override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) = - keyBinding.matchesKey(keyCode, scanCode) - } - - fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding { - override fun matches(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 index 5bca87e..91d88ca 100644 --- a/src/main/kotlin/keybindings/SavedKeyBinding.kt +++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt @@ -1,106 +1,49 @@ - - 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 net.minecraft.network.chat.Component @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<Boolean, Boolean, Boolean>) : 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<Boolean, Boolean, Boolean> { - 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() = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT) - || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SHIFT) - - } - - fun isPressed(atLeast: Boolean = false): Boolean { - if (!isBound) return false - val h = MC.window.handle - if (!InputUtil.isKeyPressed(h, keyCode)) return false - - 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) - if (atLeast) - return (ctrl >= this.ctrl) && - (alt >= this.alt) && - (shift >= this.shift) - - return (ctrl == this.ctrl) && - (alt == this.alt) && - (shift == this.shift) - } - - 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) - } - - fun format(): Text { - val stroke = Text.literal("") - if (ctrl) { - stroke.append("CTRL + ") - } - if (alt) { - stroke.append("ALT + ") - } - if (shift) { - stroke.append("SHIFT + ") // TODO: translations? - } - - stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText) - return stroke - } + val button: GenericInputButton, + val modifiers: InputModifiers, +) { + companion object { + fun isShiftDown() = InputModifiers.current().shift + + fun unbound(): SavedKeyBinding = withoutMods(GenericInputButton.unbound()) + fun withoutMods(input: GenericInputButton) = SavedKeyBinding(input, InputModifiers.none()) + fun keyWithoutMods(keyCode: Int): SavedKeyBinding = withoutMods(GenericInputButton.ofKeyCode(keyCode)) + fun keyWithMods(keyCode: Int, mods: InputModifiers) = + SavedKeyBinding(GenericInputButton.ofKeyCode(keyCode), mods) + } + + fun isPressed(atLeast: Boolean = false): Boolean { + if (!button.isPressed()) + return false + val mods = InputModifiers.current() + .without(InputModifiers.ofKey(button)) + return mods.matches(this.modifiers, atLeast) + } + + override fun toString(): String { + return format().string + } + + fun format(): Component { + val stroke = Component.empty() + if (!modifiers.isEmpty()) { + stroke.append(modifiers.format()) + stroke.append(" + ") + } + stroke.append(button.format()) + return stroke + } + + val isBound: Boolean get() = button.isBound() + fun matches(action: GenericInputAction, inputModifiers: InputModifiers, atLeast: Boolean = false): Boolean { + return action.matches(button) && this.modifiers.matches(inputModifiers, atLeast) + } } + diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt index 4b32e57..6d18223 100644 --- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -2,8 +2,10 @@ 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<SkyblockId, Set<NEURecipe>> = mapOf() @@ -17,6 +19,9 @@ class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IR .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) } } diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt index fbc9eb8..25a74de 100644 --- a/src/main/kotlin/repo/ExpLadder.kt +++ b/src/main/kotlin/repo/ExpLadder.kt @@ -19,7 +19,8 @@ object ExpLadders : IReloadable { val expInCurrentLevel: Float, var expTotal: Float, ) { - val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel } data class ExpLadder( 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/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt index 181aa70..d6be96f 100644 --- a/src/main/kotlin/repo/HypixelStaticData.kt +++ b/src/main/kotlin/repo/HypixelStaticData.kt @@ -1,23 +1,19 @@ package moe.nea.firmament.repo -import io.ktor.client.call.body -import io.ktor.client.request.get import org.apache.logging.log4j.LogManager -import org.lwjgl.glfw.GLFW import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull 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.keybindings.IKeyBinding import moe.nea.firmament.util.SkyblockId -import moe.nea.firmament.util.async.waitForInput +import moe.nea.firmament.util.net.HttpUtil object HypixelStaticData { private val logger = LogManager.getLogger("Firmament.HypixelStaticData") @@ -25,7 +21,13 @@ object HypixelStaticData { private val hypixelApiBaseUrl = "https://api.hypixel.net" var lowestBin: Map<SkyblockId, Double> = mapOf() private set - var bazaarData: Map<SkyblockId, BazaarData> = mapOf() + var avg1dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var avg3dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var avg7dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var bazaarData: Map<SkyblockId.BazaarStock, BazaarData> = mapOf() private set var collectionData: Map<String, CollectionSkillData> = mapOf() private set @@ -56,9 +58,11 @@ object HypixelStaticData { val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(), ) - fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item] - fun hasBazaarStock(item: SkyblockId): Boolean { + fun getPriceOfItem(item: SkyblockId): Double? = + bazaarData[SkyblockId.BazaarStock.fromSkyBlockId(item)]?.quickStatus?.buyPrice ?: lowestBin[item] + + fun hasBazaarStock(item: SkyblockId.BazaarStock): Boolean { return item in bazaarData } @@ -74,35 +78,43 @@ object HypixelStaticData { Firmament.coroutineScope.launch { while (true) { logger.info("Updating NEU prices") - updatePrices() - delay(10.minutes) + fetchPricesFromMoulberry() + delay(5.minutes) + } + } + Firmament.coroutineScope.launch { + while (true) { + logger.info("Updating bazaar prices") + fetchBazaarPrices() + delay(2.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<Map<SkyblockId, Double>>() + lowestBin = HttpUtil.request("$moulberryBaseUrl/lowestbin.json") + .forJson<Map<SkyblockId, Double>>().await() + avg1dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/1day.json") + .forJson<Map<SkyblockId, Double>>().await() + avg3dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/3day.json") + .forJson<Map<SkyblockId, Double>>().await() + avg7dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/7day.json") + .forJson<Map<SkyblockId, Double>>().await() } private suspend fun fetchBazaarPrices() { - val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>() + val response = HttpUtil.request("$hypixelApiBaseUrl/skyblock/bazaar").forJson<BazaarResponse>() + .await() if (!response.success) { logger.warn("Retrieved unsuccessful bazaar data") } - bazaarData = response.products.mapKeys { it.key.toRepoId() } + bazaarData = response.products } private suspend fun updateCollectionData() { val response = - Firmament.httpClient.get("$hypixelApiBaseUrl/resources/skyblock/collections").body<CollectionResponse>() + HttpUtil.request("$hypixelApiBaseUrl/resources/skyblock/collections").forJson<CollectionResponse>() + .await() if (!response.success) { logger.warn("Retrieved unsuccessful collection data") } diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index e140dd8..be07042 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -4,70 +4,83 @@ 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 io.github.notenoughupdates.moulconfig.xml.Bind import java.text.NumberFormat import java.util.UUID import java.util.concurrent.ConcurrentHashMap import org.apache.logging.log4j.LogManager -import kotlinx.coroutines.Job +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.client.resource.language.I18n -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.core.component.DataComponents +import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtString -import net.minecraft.text.Style -import net.minecraft.text.Text +import net.minecraft.nbt.StringTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.TagParser +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.MutableComponent +import net.minecraft.network.chat.Style +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.datafix.DataFixers +import net.minecraft.util.datafix.fixes.References +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.item.component.CustomData import moe.nea.firmament.Firmament -import moe.nea.firmament.gui.config.HudMeta -import moe.nea.firmament.gui.config.HudPosition -import moe.nea.firmament.gui.hud.MoulConfigHud +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.loadItemFromNbt 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<String, ItemStack> = ConcurrentHashMap() - private val df = Schemas.getFixer() + private val df = DataFixers.getDataFixer() val logger = LogManager.getLogger("${Firmament.logger.name}.ItemCache") var isFlawless = true private set - private fun NEUItem.get10809CompoundTag(): NbtCompound = NbtCompound().apply { + private fun NEUItem.get10809CompoundTag(): CompoundTag = CompoundTag().apply { put("tag", LegacyTagParser.parse(nbttag)) putString("id", minecraftItemId) putByte("Count", 1) putShort("Damage", damage.toShort()) } - private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? = + @ExpensiveItemCacheApi + private fun CompoundTag.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern) + val currentSaveVersion = SharedConstants.getCurrentVersion().dataVersion().version + + @ExpensiveItemCacheApi + fun convert189ToModern(nbtComponent: CompoundTag): CompoundTag? = try { df.update( - TypeReferences.ITEM_STACK, - Dynamic(NbtOps.INSTANCE, this), + References.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, nbtComponent), -1, - SharedConstants.getGameVersion().saveVersion.id - ).value as NbtCompound + currentSaveVersion + ).value as CompoundTag } catch (e: Exception) { isFlawless = false logger.error("Could not data fix up $this", e) @@ -84,24 +97,24 @@ object ItemCache : IReloadable { fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack { return ItemStack(Items.PAINTING).apply { - setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) + setCustomName(Component.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) appendLore( listOf( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.repo.brokenitem", (neuItem?.skyblockItemId ?: idHint) ) ) ) - set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(NbtCompound().apply { - put("ID", NbtString.of(neuItem?.skyblockItemId ?: idHint?.neuItem ?: "null")) + set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().apply { + put("ID", StringTag.valueOf(neuItem?.skyblockItemId ?: idHint?.neuItem ?: "null")) })) set(FirmamentDataComponentTypes.IS_BROKEN, true) } } - fun un189Lore(lore: String): Text { - val base = Text.literal("") + fun un189Lore(lore: String): MutableComponent { + val base = Component.literal("") base.setStyle(Style.EMPTY.withItalic(false)) var lastColorCode = Style.EMPTY var readOffset = 0 @@ -112,7 +125,7 @@ object ItemCache : IReloadable { } val text = lore.substring(readOffset, nextCode) if (text.isNotEmpty()) { - base.append(Text.literal(text).setStyle(lastColorCode)) + base.append(Component.literal(text).setStyle(lastColorCode)) } readOffset = nextCode + 2 if (nextCode + 1 < lore.length) { @@ -122,25 +135,55 @@ object ItemCache : IReloadable { if (modernFormatting.isColor) { lastColorCode = Style.EMPTY.withColor(modernFormatting) } else { - lastColorCode = lastColorCode.withFormatting(modernFormatting) + lastColorCode = lastColorCode.applyFormat(modernFormatting) } } } return base } + fun tryFindFromModernFormat(skyblockId: SkyblockId): CompoundTag? { + val overlayFile = + RepoManager.overlayData.getMostModernReadableOverlay(skyblockId, currentSaveVersion) ?: return null + val overlay = TagParser.parseCompoundFully(overlayFile.path.readText()) + val result = ExportedTestConstantMeta.SOURCE_CODEC.decode( + NbtOps.INSTANCE, overlay + ).result().getOrNull() ?: return null + val meta = result.first + return df.update( + References.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, result.second), + meta.dataVersion, + currentSaveVersion + ).value as CompoundTag + } + + @ExpensiveItemCacheApi private fun NEUItem.asItemStackNow(): ItemStack { + try { + var modernItemTag = tryFindFromModernFormat(this.skyblockId) val oldItemTag = get10809CompoundTag() - val modernItemTag = oldItemTag.transformFrom10809ToModern() - ?: return brokenItemStack(this) + 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) + loadItemFromNbt( modernItemTag) ?: return brokenItemStack(this) + if (usedOldNbt) { + val tag = oldItemTag.getCompound("tag") + val extraAttributes = tag.flatMap { it.getCompound("ExtraAttributes") } + .getOrNull() + if (extraAttributes != null) + itemInstance.set(DataComponents.CUSTOM_DATA, CustomData.of(extraAttributes)) + val itemModel = tag.flatMap { it.getString("ItemModel") }.getOrNull() + if (itemModel != null) + itemInstance.set(DataComponents.ITEM_MODEL, ResourceLocation.parse(itemModel)) + } itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } itemInstance.displayNameAccordingToNbt = un189Lore(displayName) - val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes") - if (extraAttributes != null) - itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) return itemInstance } catch (e: Exception) { e.printStackTrace() @@ -148,6 +191,11 @@ object ItemCache : IReloadable { } } + fun hasCacheFor(skyblockId: SkyblockId): Boolean { + return skyblockId.neuItem in cache + } + + @ExpensiveItemCacheApi fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map<String, String>? = null): ItemStack { if (this == null) return brokenItemStack(null, idHint) var s = cache[this.skyblockItemId] @@ -158,7 +206,7 @@ object ItemCache : IReloadable { if (!loreReplacements.isNullOrEmpty()) { s = s.copy()!! s.applyLoreReplacements(loreReplacements) - s.setCustomName(s.name.applyLoreReplacements(loreReplacements)) + s.setCustomName(s.hoverName.applyLoreReplacements(loreReplacements)) } return s } @@ -171,67 +219,59 @@ object ItemCache : IReloadable { } } - fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text { + fun Component.applyLoreReplacements(loreReplacements: Map<String, String>): Component { 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) + Component.literal(string).setStyle(it.style) } } - var job: Job? = null - - object ReloadProgressHud : MoulConfigHud( - "repo_reload", HudMeta(HudPosition(0.0, 0.0, 1F), Text.literal("Repo Reload"), 180, 18)) { - - - var isEnabled = false - override fun shouldRender(): Boolean { - return isEnabled - } + var itemRecacheScope: CoroutineScope? = null - @get:Bind("current") - var current: Double = 0.0 + private var recacheSoonSubmitted = mutableSetOf<SkyblockId>() - @get:Bind("label") - var label: String = "" - - @get:Bind("max") - var max: Double = 0.0 - - fun reportProgress(label: String, current: Int, max: Int) { - this.label = label - this.current = current.toDouble() - this.max = max.toDouble() + @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 = job - if (j != null && j.isActive) { - j.cancel() - } + val j = itemRecacheScope + j?.cancel("New reload invoked") cache.clear() isFlawless = true if (TestUtil.isInTest) return - job = Firmament.coroutineScope.launch { - val items = repository.items?.items - if (items == null) { - ReloadProgressHud.isEnabled = false - return@launch - } - val recacheItems = I18n.translate("firmament.repo.cache") - ReloadProgressHud.reportProgress(recacheItems, 0, items.size) - ReloadProgressHud.isEnabled = true - var i = 0 - items.values.forEach { - it.asItemStack() // Rebuild cache - ReloadProgressHud.reportProgress(recacheItems, i++, items.size) - } - ReloadProgressHud.isEnabled = false + 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 { @@ -249,7 +289,7 @@ object ItemCache : IReloadable { "http://textures.minecraft.net/texture/7b951fed6a7b2cbc2036916dec7a46c4a56481564d14f945b6ebc03382766d3b" } val itemStack = ItemStack(Items.PLAYER_HEAD) - itemStack.setCustomName(Text.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins")) + itemStack.setCustomName(Component.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins")) itemStack.setSkullOwner(uuid, texture) return itemStack } @@ -263,10 +303,10 @@ object ItemCache : IReloadable { } -operator fun NbtCompound.set(key: String, value: String) { +operator fun CompoundTag.set(key: String, value: String) { putString(key, value) } -operator fun NbtCompound.set(key: String, value: NbtElement) { +operator fun CompoundTag.set(key: String, value: Tag) { put(key, value) } diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt new file mode 100644 index 0000000..5b5b016 --- /dev/null +++ b/src/main/kotlin/repo/MiningRepoData.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +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.streams.asSequence +import net.minecraft.world.level.block.Block +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.ReforgeStore.kJson +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.mc.loadItemFromNbt +import moe.nea.firmament.util.skyblockId + +class MiningRepoData : IReloadable { + var customMiningAreas: Map<SkyBlockIsland, CustomMiningArea> = mapOf() + private set + var customMiningBlocks: List<CustomMiningBlock> = listOf() + private set + var toolsByBreakingPower: NavigableMap<BreakingPowerKey, SBItemStack> = Collections.emptyNavigableMap() + private set + + + data class BreakingPowerKey( + val breakingPower: Int, + val itemId: SkyblockId? = null + ) { + companion object { + val COMPARATOR: Comparator<BreakingPowerKey> = + Comparator + .comparingInt<BreakingPowerKey> { it.breakingPower } + .thenComparing(Comparator.comparing( + { it.itemId }, + nullsFirst(Comparator.comparing<SkyblockId, Boolean> { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder<SkyblockId>())))) + } + } + + 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<CustomMiningBlock>()) } + .toList() + toolsByBreakingPower = Collections.unmodifiableNavigableMap( + repo.items.items + .values + .asSequence() + .map { SBItemStack(it.skyblockId) } + .filter { it.breakingPower > 0 } + .associateTo(TreeMap<BreakingPowerKey, SBItemStack>(BreakingPowerKey.COMPARATOR)) { + BreakingPowerKey(it.breakingPower, it.skyblockId) to it + }) + } + + fun getToolsThatCanBreak(breakingPower: Int): Collection<SBItemStack> { + 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<Block189> = 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 = Component.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<SkyBlockIsland>? = 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(CompoundTag().apply { + putString("id", itemId) + putShort("Damage", damage) + }) ?: return null + val itemStack = loadItemFromNbt(newCompound) ?: 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<SkyblockId, List<OverlayFile>> = mapOf() + override fun reload(repo: NEURepository) { + val items = mutableMapOf<SkyblockId, MutableList<OverlayFile>>() + 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/Reforge.kt b/src/main/kotlin/repo/Reforge.kt index dc0d93d..5f6506f 100644 --- a/src/main/kotlin/repo/Reforge.kt +++ b/src/main/kotlin/repo/Reforge.kt @@ -13,10 +13,10 @@ 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 net.minecraft.world.item.Item +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.skyblock.ItemType @@ -64,9 +64,9 @@ data class Reforge( } jsonElement["itemId"]?.let { decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach { - val ident = Identifier.tryParse(it) + val ident = ResourceLocation.tryParse(it) if (ident != null) - filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident))) + filters.add(AllowsVanillaItemType(ResourceKey.create(Registries.ITEM, ident))) } } return filters @@ -90,8 +90,8 @@ data class Reforge( return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content)) } jsonObject["minecraftId"]?.let { - return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, - Identifier.of((it as JsonPrimitive).content))) + return AllowsVanillaItemType(ResourceKey.create(Registries.ITEM, + ResourceLocation.parse((it as JsonPrimitive).content))) } error("Unknown item type") } @@ -104,7 +104,7 @@ data class Reforge( data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter - data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter + data class AllowsVanillaItemType(val minecraftId: ResourceKey<Item>) : ReforgeEligibilityFilter } diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt index 4c01974..cf8b434 100644 --- a/src/main/kotlin/repo/ReforgeStore.kt +++ b/src/main/kotlin/repo/ReforgeStore.kt @@ -9,8 +9,8 @@ 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 net.minecraft.world.item.Item +import net.minecraft.resources.ResourceKey import moe.nea.firmament.Firmament import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId @@ -23,7 +23,7 @@ object ReforgeStore : ExtraRecipeProvider, IReloadable { } var byType: Map<ItemType, List<Reforge>> = mapOf() - var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf() + var byVanilla: Map<ResourceKey<Item>, List<Reforge>> = mapOf() var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf() var modifierLut = mapOf<ReforgeId, Reforge>() var byReforgeStone = mapOf<SkyblockId, Reforge>() @@ -52,7 +52,7 @@ object ReforgeStore : ExtraRecipeProvider, IReloadable { byReforgeStone = allReforges.filter { it.reforgeStone != null } .associateBy { it.reforgeStone!! } val byType = mutableMapOf<ItemType, MutableList<Reforge>>() - val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>() + val byVanilla = mutableMapOf<ResourceKey<Item>, MutableList<Reforge>>() val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>() this.byType = byType this.byVanilla = byVanilla diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt index 3efd83b..adbe36e 100644 --- a/src/main/kotlin/repo/RepoDownloadManager.kt +++ b/src/main/kotlin/repo/RepoDownloadManager.kt @@ -1,11 +1,5 @@ - - 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 @@ -13,6 +7,7 @@ import java.nio.file.StandardOpenOption import java.util.zip.ZipInputStream import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.future.await import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlin.io.path.createDirectories @@ -23,106 +18,112 @@ 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.repo.RepoDownloadManager.latestSavedVersionHash import moe.nea.firmament.util.iterate +import moe.nea.firmament.util.net.HttpUtil 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(): 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/${RepoManager.Config.branch}") - if (response.status.value != 200) { - return null - } - return response.body<GithubCommitsResponse>().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): Boolean = withContext(CoroutineName("Repo Update Check")) { - val latestSha = requestLatestGithubSha() - 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) } - } - } - } + 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.TConfig.branch == "prerelease") { + RepoManager.TConfig.branch = "master" + } + val response = + HttpUtil.request("https://api.github.com/repos/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/commits/${branchOverride ?: RepoManager.TConfig.branch}") + .forJson<GithubCommitsResponse>() + .await() + return response.sha + } + + private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { + val response = HttpUtil.request(url) + val targetFile = Files.createTempFile("firmament-repo", ".zip") + Files.newOutputStream(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + .use { outputStream -> + response.forInputStream().await().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + 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.TConfig.username}/${RepoManager.TConfig.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/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt index 6d9ba14..c8da6a7 100644 --- a/src/main/kotlin/repo/RepoManager.kt +++ b/src/main/kotlin/repo/RepoManager.kt @@ -7,23 +7,28 @@ 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 net.minecraft.client.MinecraftClient -import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket -import net.minecraft.recipe.display.CuttingRecipeDisplay +import kotlinx.coroutines.withContext +import net.minecraft.client.Minecraft +import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket +import net.minecraft.world.item.crafting.SelectableRecipe +import net.minecraft.util.StringRepresentable 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.tr object RepoManager { - object Config : ManagedConfig("repo", Category.META) { + @Config + object TConfig : ManagedConfig("repo", Category.META) { var username by string("username") { "NotEnoughUpdates" } var reponame by string("reponame") { "NotEnoughUpdates-REPO" } var branch by string("branch") { "master" } @@ -32,20 +37,35 @@ object RepoManager { username = "NotEnoughUpdates" reponame = "NotEnoughUpdates-REPO" branch = "master" - save() + markDirty() } - + val enableREI by toggle("enable-rei") { true } val disableItemGroups by toggle("disable-item-groups") { true } val reload by button("reload") { - save() - RepoManager.reload() + markDirty() + Firmament.coroutineScope.launch { + RepoManager.reload() + } } val redownload by button("redownload") { - save() + markDirty() 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) : StringRepresentable { + NOTHING("nothing"), + RENDER("render"), + RENDER_AND_TEXT("text"), + ; + + fun rendersPerfectText() = this == RENDER_AND_TEXT + fun rendersPerfectVisuals() = this == RENDER || this == RENDER_AND_TEXT + + override fun getSerializedName(): String? = label } val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash @@ -54,15 +74,22 @@ object RepoManager { val essenceRecipeProvider = EssenceRecipeProvider() val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore) + val miningData = MiningRepoData() + val overlayData = ModernOverlaysData() + val enchantedBookCache = EnchantedBookCache() 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) + registerReloadListener(enchantedBookCache) ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this)) registerReloadListener { if (TestUtil.isInTest) return@registerReloadListener @@ -73,7 +100,6 @@ object RepoManager { } } } - registerReloadListener(recipeCache) } } @@ -86,8 +112,8 @@ object RepoManager { fun getUsagesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.usages[skyblockId] ?: setOf() private fun trySendClientboundUpdateRecipesPacket(): Boolean { - return MinecraftClient.getInstance().world != null && MinecraftClient.getInstance().networkHandler?.onSynchronizeRecipes( - SynchronizeRecipesS2CPacket(mutableMapOf(), CuttingRecipeDisplay.Grouping.empty()) + return Minecraft.getInstance().level != null && Minecraft.getInstance().connection?.handleUpdateRecipes( + ClientboundUpdateRecipesPacket(mutableMapOf(), SelectableRecipe.SingleInputSet.empty()) ) != null } @@ -100,47 +126,45 @@ object RepoManager { 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 { - ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true - try { - RepoDownloadManager.downloadUpdate(force) - ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1) - } finally { - ItemCache.ReloadProgressHud.isEnabled = false - } + RepoDownloadManager.downloadUpdate(force) reload() } } fun reloadForTest(from: Path) { neuRepo = makeNEURepository(from) - reload() + reloadSync() } - fun reload() { - if (!TestUtil.isInTest && !MC.instance.isOnThread) { - MC.instance.send { - reload() - } - return + + suspend fun reload() { + withContext(Dispatchers.IO) { + reloadSync() } + } + + fun reloadSync() { try { - ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk", - 0, - -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true 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.") + tr( + "firmament.repo.reloadfail", + "Failed to reload repository. This will result in some mod features not working." + ) ) - ItemCache.ReloadProgressHud.isEnabled = false } } @@ -153,10 +177,12 @@ object RepoManager { return } neuRepo = makeNEURepository(RepoDownloadManager.repoSavedLocation) - if (Config.autoUpdate) { + if (TConfig.autoUpdate) { launchAsyncUpdate() } else { - reload() + Firmament.coroutineScope.launch { + reload() + } } } @@ -182,6 +208,8 @@ object RepoManager { } fun getRepoRef(): String { - return "${Config.username}/${Config.reponame}#${Config.branch}" + return "${TConfig.username}/${TConfig.reponame}#${TConfig.branch}" } + + fun shouldLoadREI(): Boolean = TConfig.enableREI } diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt index 617efec..cd0bc63 100644 --- a/src/main/kotlin/repo/RepoModResourcePack.kt +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -3,49 +3,50 @@ package moe.nea.firmament.repo import java.io.InputStream import java.nio.file.Files import java.nio.file.Path -import java.util.* +import java.util.Optional 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.PathUtil +import net.minecraft.server.packs.AbstractPackResources +import net.minecraft.server.packs.resources.IoSupplier +import net.minecraft.server.packs.resources.FallbackResourceManager +import net.minecraft.server.packs.resources.Resource +import net.minecraft.server.packs.PackResources +import net.minecraft.server.packs.PackLocationInfo +import net.minecraft.server.packs.repository.PackSource +import net.minecraft.server.packs.PackType +import net.minecraft.server.packs.resources.ResourceMetadata +import net.minecraft.server.packs.metadata.MetadataSectionType +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.FileUtil import moe.nea.firmament.Firmament class RepoModResourcePack(val basePath: Path) : ModResourcePack { companion object { - fun append(packs: MutableList<in ModResourcePack>) { + fun append(packs: ModResourcePackSorter) { Firmament.logger.info("Registering mod resource pack") - packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) } - fun createResourceDirectly(identifier: Identifier): Optional<Resource> { + fun createResourceDirectly(identifier: ResourceLocation): Optional<Resource> { val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation) return Optional.of( Resource( pack, - pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() + pack.getResource(PackType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() ) { val base = - pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) + pack.getResource(PackType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) if (base == null) - ResourceMetadata.NONE + ResourceMetadata.EMPTY else - NamespaceResourceManager.loadMetadata(base) + FallbackResourceManager.parseMetadata(base) } ) } @@ -54,32 +55,32 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { override fun close() { } - override fun openRoot(vararg segments: String): InputSupplier<InputStream>? { - return getFile(segments)?.let { InputSupplier.create(it) } + override fun getRootResource(vararg segments: String): IoSupplier<InputStream>? { + return getFile(segments)?.let { IoSupplier.create(it) } } fun getFile(segments: Array<out String>): Path? { - PathUtil.validatePath(*segments) + FileUtil.validatePath(*segments) val path = segments.fold(basePath, Path::resolve) if (!path.isRegularFile()) return null return path } - override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? { - if (type != ResourceType.CLIENT_RESOURCES) return null + override fun getResource(type: PackType?, id: ResourceLocation): IoSupplier<InputStream>? { + if (type != PackType.CLIENT_RESOURCES) return null if (id.namespace != "neurepo") return null val file = getFile(id.path.split("/").toTypedArray()) - return file?.let { InputSupplier.create(it) } + return file?.let { IoSupplier.create(it) } } - override fun findResources( - type: ResourceType?, + override fun listResources( + type: PackType?, namespace: String, prefix: String, - consumer: ResourcePack.ResultConsumer + consumer: PackResources.ResourceOutput ) { if (namespace != "neurepo") return - if (type != ResourceType.CLIENT_RESOURCES) return + if (type != PackType.CLIENT_RESOURCES) return val prefixPath = basePath.resolve(prefix) if (!prefixPath.exists()) @@ -88,17 +89,20 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { .asSequence() .map { it.relativeTo(basePath) } .forEach { - consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it)) + consumer.accept( + ResourceLocation.tryBuild("neurepo", it.toString()) ?: return@forEach, + IoSupplier.create(it) + ) } } - override fun getNamespaces(type: ResourceType?): Set<String> { - if (type != ResourceType.CLIENT_RESOURCES) return emptySet() + override fun getNamespaces(type: PackType?): Set<String> { + if (type != PackType.CLIENT_RESOURCES) return emptySet() return setOf("neurepo") } - override fun <T : Any?> parseMetadata(metadataSerializer: ResourceMetadataSerializer<T>?): T? { - return AbstractFileResourcePack.parseMetadata( + override fun <T : Any?> getMetadataSection(metadataSerializer: MetadataSectionType<T>?): T? { + return AbstractPackResources.getMetadataFromStream( metadataSerializer, """ { "pack": { @@ -106,13 +110,13 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { "description": "NEU Repo Resources" } } -""".trimIndent().byteInputStream() +""".trimIndent().byteInputStream(), location() ) } - override fun getInfo(): ResourcePackInfo { - return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty()) + override fun location(): PackLocationInfo { + return PackLocationInfo("neurepo", Component.literal("NEU Repo"), PackSource.BUILT_IN, Optional.empty()) } override fun getFabricModMetadata(): ModMetadata { diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index 4d07801..89e53e8 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -5,20 +5,22 @@ 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 net.minecraft.world.item.ItemStack +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.TextColor +import net.minecraft.ChatFormatting 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 @@ -27,12 +29,17 @@ 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 @@ -41,7 +48,7 @@ data class SBItemStack constructor( val neuItem: NEUItem?, private var stackSize: Int, private var petData: PetData?, - val extraLore: List<Text> = emptyList(), + val extraLore: List<Component> = emptyList(), val stars: Int = 0, val fallback: ItemStack? = null, val reforge: ReforgeId? = null, @@ -60,9 +67,9 @@ data class SBItemStack constructor( } companion object { - val PACKET_CODEC: PacketCodec<in RegistryByteBuf, SBItemStack> = PacketCodec.tuple( + val PACKET_CODEC: StreamCodec<in RegistryFriendlyByteBuf, SBItemStack> = StreamCodec.composite( SkyblockId.PACKET_CODEC, { it.skyblockId }, - PacketCodecs.VAR_INT, { it.stackSize }, + ByteBufCodecs.VAR_INT, { it.stackSize }, { id, count -> SBItemStack(id, count) } ) val CODEC: Codec<SBItemStack> = RecordCodecBuilder.create { @@ -75,6 +82,7 @@ data class SBItemStack constructor( } val EMPTY = SBItemStack(SkyblockId.NULL, 0) + private val BREAKING_POWER_REGEX = "Breaking Power (?<power>[0-9]+)".toPattern() operator fun invoke(itemStack: ItemStack): SBItemStack { val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL return SBItemStack( @@ -99,6 +107,13 @@ data class SBItemStack constructor( return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack) } + fun parseStatBlock(itemStack: ItemStack): List<StatLine> { + return itemStack.loreAccordingToNbt + .map { parseStatLine(it) } + .takeWhile { it != null } + .filterNotNull() + } + fun appendEnhancedStats( itemStack: ItemStack, reforgeStats: Map<String, Double>, @@ -121,7 +136,7 @@ data class SBItemStack constructor( loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute() } if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) { - loreMut.add(0, Text.literal("")) + loreMut.add(0, Component.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. @@ -134,46 +149,47 @@ data class SBItemStack constructor( data class StatFormatting( val postFix: String, - val color: Formatting, + val color: ChatFormatting, + 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.RED), - "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), + "Sea Creature Chance" to StatFormatting("%", ChatFormatting.RED), + "Strength" to StatFormatting("", ChatFormatting.RED), + "Damage" to StatFormatting("", ChatFormatting.RED), + "Bonus Attack Speed" to StatFormatting("%", ChatFormatting.RED), + "Shot Cooldown" to StatFormatting("s", ChatFormatting.GREEN, false), + "Ability Damage" to StatFormatting("%", ChatFormatting.RED), + "Crit Damage" to StatFormatting("%", ChatFormatting.RED), + "Crit Chance" to StatFormatting("%", ChatFormatting.RED), + "Ability Damage" to StatFormatting("%", ChatFormatting.RED), + "Trophy Fish Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Health" to StatFormatting("", ChatFormatting.GREEN), + "Defense" to StatFormatting("", ChatFormatting.GREEN), + "Fishing Speed" to StatFormatting("", ChatFormatting.GREEN), + "Double Hook Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Mining Speed" to StatFormatting("", ChatFormatting.GREEN), + "Mining Fortune" to StatFormatting("", ChatFormatting.GREEN), + "Heat Resistance" to StatFormatting("", ChatFormatting.GREEN), + "Swing Range" to StatFormatting("", ChatFormatting.GREEN), + "Rift Time" to StatFormatting("", ChatFormatting.GREEN), + "Speed" to StatFormatting("", ChatFormatting.GREEN), + "Farming Fortune" to StatFormatting("", ChatFormatting.GREEN), + "True Defense" to StatFormatting("", ChatFormatting.GREEN), + "Mending" to StatFormatting("", ChatFormatting.GREEN), + "Foraging Wisdom" to StatFormatting("", ChatFormatting.GREEN), + "Farming Wisdom" to StatFormatting("", ChatFormatting.GREEN), + "Foraging Fortune" to StatFormatting("", ChatFormatting.GREEN), + "Magic Find" to StatFormatting("", ChatFormatting.GREEN), + "Ferocity" to StatFormatting("", ChatFormatting.GREEN), + "Bonus Pest Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Cold Resistance" to StatFormatting("", ChatFormatting.GREEN), + "Pet Luck" to StatFormatting("", ChatFormatting.GREEN), + "Fear" to StatFormatting("", ChatFormatting.GREEN), + "Mana Regen" to StatFormatting("%", ChatFormatting.GREEN), + "Rift Damage" to StatFormatting("", ChatFormatting.GREEN), + "Hearts" to StatFormatting("", ChatFormatting.GREEN), + "Vitality" to StatFormatting("", ChatFormatting.GREEN), // TODO: make this a repo json ) @@ -181,20 +197,22 @@ data class SBItemStack constructor( private val statLabelRegex = "(?<statName>.*): ".toPattern() enum class BuffKind( - val color: Formatting, + val color: ChatFormatting, val prefix: String, val postFix: String, + val isHidden: Boolean, ) { - REFORGE(Formatting.BLUE, "(", ")"), - + REFORGE(ChatFormatting.BLUE, "(", ")", false), + STAR_BUFF(ChatFormatting.RESET, "", "", true), + CATA_STAR_BUFF(ChatFormatting.DARK_GRAY, "(", ")", false), ; } data class StatLine( val statName: String, - val value: Text?, - val rest: List<Text> = listOf(), - val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull() + val value: Component?, + val rest: List<Component> = listOf(), + val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.toDoubleOrNull() ) { fun addStat(amount: Double, buffKind: BuffKind): StatLine { val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true) @@ -202,21 +220,29 @@ data class SBItemStack constructor( valueNum = (valueNum ?: 0.0) + amount, value = null, rest = rest + - listOf( - Text.literal( + if (buffKind.isHidden) emptyList() + else listOf( + Component.literal( buffKind.prefix + formattedAmount + statFormatting.postFix + - buffKind.postFix + " ") - .withColor(buffKind.color))) + buffKind.postFix + " " + ) + .withColor(buffKind.color) + ) + ) } fun formatValue() = - Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0, - 1, - includeSign = true) + statFormatting.postFix + " ") + Component.literal( + FirmFormatters.formatCommas( + valueNum ?: 0.0, + 1, + includeSign = true + ) + statFormatting.postFix + " " + ) .setStyle(Style.EMPTY.withColor(statFormatting.color)) - val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN) + val statFormatting = formattingOverrides[statName] ?: StatFormatting("", ChatFormatting.GREEN) private fun abbreviate(abbreviateTo: Int): String { if (abbreviateTo >= statName.length) return statName val segments = statName.split(" ") @@ -225,9 +251,9 @@ data class SBItemStack constructor( } } - fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text = - Text.literal("").setStyle(Style.EMPTY.withItalic(false)) - .append(Text.literal("${abbreviate(abbreviateTo)}: ").grey()) + fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Component = + Component.literal("").setStyle(Style.EMPTY.withItalic(false)) + .append(Component.literal("${abbreviate(abbreviateTo)}: ").grey()) .append(value ?: formatValue()) .also { rest.forEach(it::append) } } @@ -237,10 +263,10 @@ data class SBItemStack constructor( return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } } } - private fun parseStatLine(line: Text): StatLine? { + fun parseStatLine(line: Component): StatLine? { val sibs = line.siblings val stat = sibs.firstOrNull() ?: return null - if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null + if (stat.style.color != TextColor.fromLegacyFormat(ChatFormatting.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)) @@ -303,19 +329,47 @@ data class SBItemStack constructor( 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)) + .prepend( + Component.literal(reforge.reforgeName + " ").withStyle(Rarity.colourMap[rarity] ?: ChatFormatting.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<Component>() + newList.addAll(it.subList(0, lastBlank)) + newList.add(Component.literal("")) + newList.add(Component.literal("${reforge.reforgeName} Bonus").blue()) + MC.font.splitter.splitLines(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 { @@ -325,12 +379,14 @@ data class SBItemStack constructor( return@run ItemStack.EMPTY val replacementData = mutableMapOf<String, String>() injectReplacementDataForPets(replacementData) - return@run neuItem.asItemStack(idHint = skyblockId, replacementData) + val baseItem = neuItem.asItemStack(idHint = skyblockId, replacementData) .withFallback(fallback) .copyWithCount(stackSize) - .also { appendReforgeInfo(it) } - .also { it.appendLore(extraLore) } - .also { enhanceStatsByStars(it, stars) } + val baseStats = parseStatBlock(baseItem) + appendReforgeInfo(baseItem) + baseItem.appendLore(extraLore) + enhanceStatsByStars(baseItem, stars, baseStats) + return@run baseItem } if (itemStack_ == null) itemStack_ = itemStack @@ -338,38 +394,74 @@ data class SBItemStack constructor( } - private fun starString(stars: Int): Text { - if (stars <= 0) return Text.empty() + /** + * estimate the lore content without creating an itemstack instance + */ + fun estimateLore(): List<Component> { + return (neuItem?.lore?.map { ItemCache.un189Lore(it) } ?: emptyList()) + extraLore + } + + private fun starString(stars: Int): Component { + if (stars <= 0) return Component.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) + if (stars > tiers.size * maxStars) return Component.literal(" ${stars}✪").withColor(ChatFormatting.RED) val starBaseTier = (stars - 1) / maxStars val starBaseColor = tiers[starBaseTier] val starsInCurrentTier = stars - starBaseTier * maxStars - val starString = Text.literal(" " + "✪".repeat(starsInCurrentTier)).withColor(starBaseColor.modern) + val starString = Component.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)) + starString.append(Component.literal("✪".repeat(starsInLastTier)).withColor(starLastTier.modern)) } return starString } - private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int) { + private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int, baseStats: List<StatLine>) { 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 } - fun asImmutableItemStack(): ItemStack { + @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 index 9a1aea5..84f1f48 100644 --- a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt @@ -3,17 +3,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 net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.repo.SBItemStack -interface GenericRecipeRenderer<T : NEURecipe> { - fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter) +interface GenericRecipeRenderer<T : Any> { + fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?) fun getInputs(recipe: T): Collection<SBItemStack> fun getOutputs(recipe: T): Collection<SBItemStack> val icon: ItemStack - val title: Text - val identifier: Identifier + val title: Component + val identifier: ResourceLocation fun findAllRecipes(neuRepository: NEURepository): Iterable<T> + fun discoverExtraRecipes(neuRepository: NEURepository, itemStack: SBItemStack, mustBeInOutputs: Boolean): Iterable<T> = emptyList() + val displayHeight: Int get() = 66 + val displayWidth: Int get() = 150 + val typ: Class<T> } diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt index 109bff5..7a63941 100644 --- a/src/main/kotlin/repo/recipes/RecipeLayouter.kt +++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt @@ -1,7 +1,12 @@ package moe.nea.firmament.repo.recipes import io.github.notenoughupdates.moulconfig.gui.GuiComponent -import net.minecraft.text.Text +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.npc.Villager import moe.nea.firmament.repo.SBItemStack interface RecipeLayouter { @@ -13,21 +18,49 @@ interface RecipeLayouter { * 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, + DISPLAY,; + val isBig get() = this == BIG_OUTPUT } + + fun createCyclingItemSlot( + x: Int, y: Int, + content: List<SBItemStack>, + slotKind: SlotKind + ): CyclingItemSlot + fun createItemSlot( x: Int, y: Int, content: SBItemStack?, slotKind: SlotKind, - ) + ): ItemSlot = createCyclingItemSlot(x, y, listOfNotNull(content), slotKind) + + interface CyclingItemSlot : ItemSlot { + fun onUpdate(action: () -> Unit) + } + + interface ItemSlot : Updater<SBItemStack> { + fun current(): SBItemStack + } + + interface Updater<T> { + fun update(newValue: T) + } + + fun createTooltip(rectangle: Rectangle, label: List<Component>) + fun createTooltip(rectangle: Rectangle, vararg label: Component) = + createTooltip(rectangle, label.toList()) + fun createLabel( x: Int, y: Int, - text: Text - ) + text: Component + ): Updater<Component> - fun createArrow(x: Int, y: Int) + fun createArrow(x: Int, y: Int): Rectangle fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent) + fun createFire(point: Point, animationTicks: Int) + fun createEntity(rectangle: Rectangle, entity: LivingEntity) } diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt index 679aec8..0a0d5e2 100644 --- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt @@ -4,25 +4,40 @@ 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 net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.tr -class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { - override fun render(recipe: NEUCraftingRecipe, bounds: Rectangle, layouter: RecipeLayouter) { +object SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { + 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) + val arrow = layouter.createArrow(point.x + 60, point.y + 18) + + if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) { + layouter.createTooltip( + arrow, + Component.nullToEmpty(recipe.extraText!!), + ) + } + 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 + 1 + i * 18, + point.y + 1 + j * 18, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT + ) } } layouter.createItemSlot( @@ -32,6 +47,9 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { ) } + override val typ: Class<NEUCraftingRecipe> + get() = NEUCraftingRecipe::class.java + override fun getInputs(recipe: NEUCraftingRecipe): Collection<SBItemStack> { return recipe.allInputs.mapNotNull { SBItemStack(it) } } @@ -45,6 +63,6 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { } override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE) - override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") // TODO: fix tr not being included in jars - override val identifier: Identifier = Firmament.identifier("crafting_recipe") + override val title: Component = tr("firmament.category.crafting", "SkyBlock Crafting") + override val identifier: ResourceLocation = 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..15785bd --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt @@ -0,0 +1,74 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import me.shedaniel.math.Rectangle +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +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<EssenceRecipeProvider.EssenceUpgradeRecipe> { + 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<SBItemStack> { + return recipe.allInputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.itemId)) + } + + @OptIn(ExpensiveItemCacheApi::class) + override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack() + override val title: Component = tr("firmament.category.essence", "Essence Upgrades") + override val identifier: ResourceLocation = Firmament.identifier("essence_upgrade") + override fun findAllRecipes(neuRepository: NEURepository): Iterable<EssenceRecipeProvider.EssenceUpgradeRecipe> { + return RepoManager.essenceRecipeProvider.recipes + } + + override val typ: Class<EssenceRecipeProvider.EssenceUpgradeRecipe> + 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..b595f07 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt @@ -0,0 +1,88 @@ +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 kotlin.math.cos +import kotlin.math.sin +import kotlin.time.Duration.Companion.seconds +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.tr + +object SBForgeRecipeRenderer : GenericRecipeRenderer<NEUForgeRecipe> { + override fun render( + recipe: NEUForgeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { + val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2) + val tooltip = Component.empty() + .append(Component.translatableEscape( + "firmament.recipe.forge.time", + recipe.duration.seconds, + )) + + if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) { + tooltip + .append(Component.nullToEmpty("\n")) + .append(Component.nullToEmpty(recipe.extraText)) + } + + layouter.createTooltip(arrow, tooltip) + + 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<SBItemStack> { + return recipe.inputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: NEUForgeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.outputStack)) + } + + override val icon: ItemStack = ItemStack(Blocks.ANVIL) + override val title: Component = tr("firmament.category.forge", "Forge Recipes") + override val identifier: ResourceLocation = Firmament.identifier("forge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable<NEUForgeRecipe> { + // TODO: theres gotta be an index for these tbh. + return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance<NEUForgeRecipe>() + } + + override val typ: Class<NEUForgeRecipe> + get() = NEUForgeRecipe::class.java +} diff --git a/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt new file mode 100644 index 0000000..c841dd9 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt @@ -0,0 +1,167 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.EntitySpawnReason +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.npc.VillagerProfession +import net.minecraft.world.item.ItemStack +import moe.nea.firmament.Firmament +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.SBItemStack +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.gold +import moe.nea.firmament.util.grey +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 + +object SBReforgeRecipeRenderer : GenericRecipeRenderer<Reforge> { + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + recipe: Reforge, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack? + ) { + val inputSlot = layouter.createCyclingItemSlot( + bounds.minX + 10, bounds.centerY - 9, + if (mainItem != null) listOf(mainItem) + else generateAllItems(recipe), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + val outputSlut = layouter.createItemSlot( + bounds.minX + 10 + 24 + 24, bounds.centerY - 9, + null, + RecipeLayouter.SlotKind.SMALL_OUTPUT + ) + val statLines = mutableListOf<Pair<String, RecipeLayouter.Updater<Component>>>() + for ((i, statId) in recipe.statUniverse.withIndex()) { + val label = layouter.createLabel( + bounds.minX + 10 + 24 + 24 + 20, bounds.minY + 8 + i * 11, + Component.empty() + ) + statLines.add(statId to label) + } + + fun updateOutput() { + val currentBaseItem = inputSlot.current() + outputSlut.update(currentBaseItem.copy(reforge = recipe.reforgeId)) + val stats = recipe.reforgeStats?.get(currentBaseItem.rarity) ?: mapOf() + for ((stat, label) in statLines) { + label.update( + SBItemStack.Companion.StatLine( + SBItemStack.statIdToName(stat), null, + valueNum = stats[stat] + ).reconstitute(7) + ) + } + } + + if (recipe.reforgeStone != null) { + layouter.createItemSlot( + bounds.minX + 10 + 24, bounds.centerY - 9 - 10, + SBItemStack(recipe.reforgeStone), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + val d = Rectangle( + bounds.minX + 10 + 24, bounds.centerY - 9 + 10, + 16, 16 + ) + layouter.createItemSlot( + d.x, d.y, + SBItemStack(SkyBlockItems.REFORGE_ANVIL), + RecipeLayouter.SlotKind.DISPLAY + ) + layouter.createTooltip( + d, + Rarity.entries.mapNotNull { rarity -> + recipe.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Component.literal("") + .append(rarity.text) + .append(": ") + .append(Component.literal("${formatCommas(cost, 0)} Coins").gold()) + } + ) + } else { + val entity = EntityType.VILLAGER.create(EntityRenderer.fakeWorld, EntitySpawnReason.COMMAND) + ?.also { + it.villagerData = + it.villagerData.withProfession( + MC.currentOrDefaultRegistries, + VillagerProfession.WEAPONSMITH + ) + } + val dim = EntityRenderer.defaultSize + val d = Rectangle( + Point(bounds.minX + 10 + 24 + 8 - dim.width / 2, bounds.centerY - dim.height / 2), + dim + ) + if (entity != null) + layouter.createEntity( + d, + entity + ) + layouter.createTooltip( + d, + tr( + "firmament.recipecategory.reforge.basic", + "This is a basic reforge, available at the Blacksmith." + ).grey() + ) + } + } + + private fun generateAllItems(recipe: Reforge): List<SBItemStack> { + return recipe.eligibleItems.flatMap { + when (it) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> listOf(SBItemStack(it.internalName)) + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> + ReforgeStore.resolveItemType(it.itemType) + .flatMapTo(mutableSetOf()) { itemType -> + listOf(itemType, itemType.dungeonVariant) + } + .flatMapTo(mutableSetOf()) { itemType -> + RepoItemTypeCache.byItemType[itemType] ?: listOf() + } + .map { SBItemStack(it.skyblockId) } + + is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> listOf() + } + } + } + + override fun getInputs(recipe: Reforge): Collection<SBItemStack> { + val reforgeStone = recipe.reforgeStone ?: return emptyList() + return listOf(SBItemStack(reforgeStone)) + } + + override fun getOutputs(recipe: Reforge): Collection<SBItemStack> { + return listOf() + } + + @OptIn(ExpensiveItemCacheApi::class) + override val icon: ItemStack + get() = SBItemStack(SkyBlockItems.REFORGE_ANVIL).asImmutableItemStack() + override val title: Component + get() = tr("firmament.recipecategory.reforge", "Reforge") + override val identifier: ResourceLocation + get() = Firmament.identifier("reforge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable<Reforge> { + return ReforgeStore.allReforges + } + + override val typ: Class<Reforge> + get() = Reforge::class.java +} diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt index 44bcdfd..0b7b3ea 100644 --- a/src/main/kotlin/util/Base64Util.kt +++ b/src/main/kotlin/util/Base64Util.kt @@ -1,10 +1,23 @@ - package moe.nea.firmament.util +import java.util.Base64 + object Base64Util { - fun String.padToValidBase64(): String { - val align = this.length % 4 - if (align == 0) return this - return this + "=".repeat(4 - align) - } + fun decodeString(str: String): String { + return decodeBytes(str).decodeToString() + } + + fun decodeBytes(str: String): ByteArray { + return Base64.getDecoder().decode(str.padToValidBase64()) + } + + fun String.padToValidBase64(): String { + val align = this.length % 4 + if (align == 0) return this + return this + "=".repeat(4 - align) + } + + fun encodeToString(bytes: ByteArray): String { + return Base64.getEncoder().encodeToString(bytes) + } } diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt index 002eedb..13b6d95 100644 --- a/src/main/kotlin/util/BazaarPriceStrategy.kt +++ b/src/main/kotlin/util/BazaarPriceStrategy.kt @@ -9,7 +9,7 @@ enum class BazaarPriceStrategy { NPC_SELL; fun getSellPrice(skyblockId: SkyblockId): Double { - val bazaarEntry = HypixelStaticData.bazaarData[skyblockId] ?: return 0.0 + val bazaarEntry = HypixelStaticData.bazaarData[skyblockId.asBazaarStock] ?: return 0.0 return when (this) { BUY_ORDER -> bazaarEntry.quickStatus.sellPrice SELL_ORDER -> bazaarEntry.quickStatus.buyPrice 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/CommonSoundEffects.kt b/src/main/kotlin/util/CommonSoundEffects.kt index a97a2cb..a4d7129 100644 --- a/src/main/kotlin/util/CommonSoundEffects.kt +++ b/src/main/kotlin/util/CommonSoundEffects.kt @@ -2,18 +2,18 @@ package moe.nea.firmament.util -import net.minecraft.client.sound.PositionedSoundInstance -import net.minecraft.sound.SoundEvent -import net.minecraft.util.Identifier +import net.minecraft.client.resources.sounds.SimpleSoundInstance +import net.minecraft.sounds.SoundEvent +import net.minecraft.resources.ResourceLocation // 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 playSound(identifier: ResourceLocation) { + MC.soundManager.play(SimpleSoundInstance.forUI(SoundEvent.createVariableRangeEvent(identifier), 1F)) } fun playFailure() { - playSound(Identifier.of("minecraft", "block.anvil.place")) + playSound(ResourceLocation.fromNamespaceAndPath("minecraft", "block.anvil.place")) } fun playSuccess() { @@ -21,6 +21,6 @@ object CommonSoundEffects { } fun playDing() { - playSound(Identifier.of("minecraft", "entity.arrow.hit_player")) + playSound(ResourceLocation.fromNamespaceAndPath("minecraft", "entity.arrow.hit_player")) } } diff --git a/src/main/kotlin/util/DurabilityBarEvent.kt b/src/main/kotlin/util/DurabilityBarEvent.kt index 993462c..c2f863f 100644 --- a/src/main/kotlin/util/DurabilityBarEvent.kt +++ b/src/main/kotlin/util/DurabilityBarEvent.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.util import me.shedaniel.math.Color -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack import moe.nea.firmament.events.FirmamentEvent import moe.nea.firmament.events.FirmamentEventBus diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt index 190381d..3db4ecd 100644 --- a/src/main/kotlin/util/ErrorUtil.kt +++ b/src/main/kotlin/util/ErrorUtil.kt @@ -29,15 +29,31 @@ object ErrorUtil { inline fun softError(message: String, exception: Throwable) { if (aggressiveErrors) throw IllegalStateException(message, exception) - else Firmament.logger.error(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 Firmament.logger.error(message) + else logError(message) + } + + fun <T> Result<T>.intoCatch(message: String): Catch<T> { + return this.map { Catch.succeed(it) }.getOrElse { + softError(message, it) + Catch.fail(it) + } } class Catch<T> 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) @@ -73,4 +89,9 @@ object ErrorUtil { 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 index 4b32c2a..c4cffc3 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -9,11 +9,40 @@ 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.network.chat.Component +import net.minecraft.core.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) { @@ -74,7 +103,7 @@ object FirmFormatters { return sb.toString() } - fun debugPath(path: Path): Text { + fun debugPath(path: Path): Component { if (!path.exists()) { return tr("firmament.path.missing", "$path (missing)").red() } @@ -98,9 +127,16 @@ object FirmFormatters { fun formatBool( boolean: Boolean, trueIsGood: Boolean = true, - ): Text { - val text = Text.literal(boolean.toString()) + ): Component { + val text = Component.literal(boolean.toString()) return if (boolean == trueIsGood) text.lime() else text.red() } + fun formatPosition(position: BlockPos): Component { + return Component.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 index 5e13d51..8797a31 100644 --- a/src/main/kotlin/util/FragmentGuiScreen.kt +++ b/src/main/kotlin/util/FragmentGuiScreen.kt @@ -6,26 +6,25 @@ 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 +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.network.chat.Component abstract class FragmentGuiScreen( val dismissOnOutOfBounds: Boolean = true -) : Screen(Text.literal("")) { +) : Screen(Component.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() - } + fun renderPopup(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + popup?.render(context, mouseX, mouseY, delta) + } private inline fun ifPopup(ifYes: (MoulConfigFragment) -> Unit): Boolean { val p = popup ?: return false @@ -33,15 +32,15 @@ abstract class FragmentGuiScreen( return true } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyPressed(input: KeyEvent): Boolean { return ifPopup { - it.keyPressed(keyCode, scanCode, modifiers) + it.keyPressed(input) } } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyReleased(input: KeyEvent): Boolean { return ifPopup { - it.keyReleased(keyCode, scanCode, modifiers) + it.keyReleased(input) } } @@ -49,35 +48,35 @@ abstract class FragmentGuiScreen( ifPopup { it.mouseMoved(mouseX, mouseY) } } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseReleased(click: MouseButtonEvent): Boolean { return ifPopup { - it.mouseReleased(mouseX, mouseY, button) + it.mouseReleased(click) } } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { return ifPopup { - it.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + it.mouseDragged(click, offsetX, offsetY) } } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { return ifPopup { if (!Rectangle( it.position, - Dimension(it.context.root.width, it.context.root.height) - ).contains(Point(mouseX, mouseY)) + Dimension(it.guiContext.root.width, it.guiContext.root.height) + ).contains(Point(click.x, click.y)) && dismissOnOutOfBounds ) { popup = null } else { - it.mouseClicked(mouseX, mouseY, button) + it.mouseClicked(click, doubled) } - }|| super.mouseClicked(mouseX, mouseY, button) + }|| super.mouseClicked(click, doubled) } - override fun charTyped(chr: Char, modifiers: Int): Boolean { - return ifPopup { it.charTyped(chr, modifiers) } + override fun charTyped(input: CharacterEvent): Boolean { + return ifPopup { it.charTyped(input) } } override fun mouseScrolled( diff --git a/src/main/kotlin/util/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt index a2e4ad2..91202dd 100644 --- a/src/main/kotlin/util/HoveredItemStack.kt +++ b/src/main/kotlin/util/HoveredItemStack.kt @@ -1,27 +1,51 @@ 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 kotlin.jvm.optionals.getOrNull +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.item.ItemStack +import moe.nea.firmament.api.v1.FirmamentAPI import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.compatloader.CompatLoader -interface HoveredItemStackProvider { - fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? +interface HoveredItemStackProvider : Comparable<HoveredItemStackProvider> { + fun provideHoveredItemStack(screen: Screen): ItemStack? + override fun compareTo(other: HoveredItemStackProvider): Int { + return compareValues(this.prio, other.prio) + } + + val prio: Int get() = 0 - companion object : CompatLoader<HoveredItemStackProvider>(HoveredItemStackProvider::class) + companion object : CompatLoader<HoveredItemStackProvider>(HoveredItemStackProvider::class) { + val sorted = HoveredItemStackProvider.allValidInstances.sorted() + } } @AutoService(HoveredItemStackProvider::class) class VanillaScreenProvider : HoveredItemStackProvider { - override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? { - screen as AccessorHandledScreen - val vanillaSlot = screen.focusedSlot_Firmament?.stack + + override fun provideHoveredItemStack(screen: Screen): ItemStack? { + if (screen !is AccessorHandledScreen) return null + val vanillaSlot = screen.focusedSlot_Firmament?.item return vanillaSlot } + + override val prio: Int + get() = -1 +} + +@AutoService(HoveredItemStackProvider::class) +class FirmamentStackScreenProvider : HoveredItemStackProvider { + override fun provideHoveredItemStack(screen: Screen): ItemStack? { + return FirmamentAPI.getInstance() + .hoveredItemWidget + .getOrNull() + ?.itemStack + } } -val HandledScreen<*>.focusedItemStack: ItemStack? +val Screen.focusedItemStack: ItemStack? get() = - HoveredItemStackProvider.allValidInstances - .firstNotNullOfOrNull { it.provideHoveredItemStack(this) } + HoveredItemStackProvider.sorted + .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } } diff --git a/src/main/kotlin/util/IdentifierSerializer.kt b/src/main/kotlin/util/IdentifierSerializer.kt index 65c5b1c..2255255 100644 --- a/src/main/kotlin/util/IdentifierSerializer.kt +++ b/src/main/kotlin/util/IdentifierSerializer.kt @@ -8,18 +8,18 @@ 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 +import net.minecraft.resources.ResourceLocation -object IdentifierSerializer : KSerializer<Identifier> { +object IdentifierSerializer : KSerializer<ResourceLocation> { 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 deserialize(decoder: Decoder): ResourceLocation { + return ResourceLocation.parse(decoder.decodeSerializableValue(delegateSerializer)) } - override fun serialize(encoder: Encoder, value: Identifier) { + override fun serialize(encoder: Encoder, value: ResourceLocation) { encoder.encodeSerializableValue(delegateSerializer, value.toString()) } } 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/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt index 1a5d1dd..20574d8 100644 --- a/src/main/kotlin/util/LegacyFormattingCode.kt +++ b/src/main/kotlin/util/LegacyFormattingCode.kt @@ -1,6 +1,6 @@ package moe.nea.firmament.util -import net.minecraft.util.Formatting +import net.minecraft.ChatFormatting enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) { BLACK("BLACK", '0', 0), @@ -30,7 +30,7 @@ enum class LegacyFormattingCode(val label: String, val char: Char, val index: In val byCode = entries.associateBy { it.char } } - val modern = Formatting.byCode(char)!! + val modern = ChatFormatting.getByCode(char)!! val formattingCode = "§$char" diff --git a/src/main/kotlin/util/LegacyTagParser.kt b/src/main/kotlin/util/LegacyTagParser.kt index 4e08da1..0de4caa 100644 --- a/src/main/kotlin/util/LegacyTagParser.kt +++ b/src/main/kotlin/util/LegacyTagParser.kt @@ -2,18 +2,18 @@ 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 +import java.util.Stack +import net.minecraft.nbt.NumericTag +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.DoubleTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.FloatTag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.LongTag +import net.minecraft.nbt.ShortTag +import net.minecraft.nbt.StringTag class LegacyTagParser private constructor(string: String) { data class TagParsingException(val baseString: String, val offset: Int, val mes0: String) : @@ -93,7 +93,7 @@ class LegacyTagParser private constructor(string: String) { companion object { val digitRange = "0123456789-" - fun parse(string: String): NbtCompound { + fun parse(string: String): CompoundTag { return LegacyTagParser(string).baseTag } } @@ -102,11 +102,11 @@ class LegacyTagParser private constructor(string: String) { racer.consumeWhile { Character.isWhitespace(it.last()) } // Only check last since other chars are always checked before. } - fun parseTag(): NbtCompound { + fun parseTag(): CompoundTag { skipWhitespace() racer.expect("{", "Expected '{’ at start of tag") skipWhitespace() - val tag = NbtCompound() + val tag = CompoundTag() while (!racer.tryConsume("}")) { skipWhitespace() val lhs = parseIdentifier() @@ -121,7 +121,7 @@ class LegacyTagParser private constructor(string: String) { return tag } - private fun parseAny(): NbtElement { + private fun parseAny(): Tag { skipWhitespace() val nextChar = racer.peekReq(1) ?: racer.error("Expected new object, found EOF") return when { @@ -133,11 +133,11 @@ class LegacyTagParser private constructor(string: String) { } } - fun parseList(): NbtList { + fun parseList(): ListTag { skipWhitespace() racer.expect("[", "Expected '[' at start of tag") skipWhitespace() - val list = NbtList() + val list = ListTag() while (!racer.tryConsume("]")) { skipWhitespace() racer.pushState() @@ -183,8 +183,8 @@ class LegacyTagParser private constructor(string: String) { return sb.toString() } - fun parseStringTag(): NbtString { - return NbtString.of(parseQuotedString()) + fun parseStringTag(): StringTag { + return StringTag.valueOf(parseQuotedString()) } object Patterns { @@ -198,7 +198,7 @@ class LegacyTagParser private constructor(string: String) { val ROUGH_PATTERN = "[-+]?[0-9]*\\.?[0-9]*[dDbBfFlLsS]?".toRegex() } - fun parseNumericTag(): AbstractNbtNumber { + fun parseNumericTag(): NumericTag { skipWhitespace() val textForm = racer.consumeWhile { Patterns.ROUGH_PATTERN.matchEntire(it) != null } if (textForm.isEmpty()) { @@ -206,27 +206,27 @@ class LegacyTagParser private constructor(string: String) { } val floatMatch = Patterns.FLOAT.matchEntire(textForm) if (floatMatch != null) { - return NbtFloat.of(floatMatch.groups[1]!!.value.toFloat()) + return FloatTag.valueOf(floatMatch.groups[1]!!.value.toFloat()) } val byteMatch = Patterns.BYTE.matchEntire(textForm) if (byteMatch != null) { - return NbtByte.of(byteMatch.groups[1]!!.value.toByte()) + return ByteTag.valueOf(byteMatch.groups[1]!!.value.toByte()) } val longMatch = Patterns.LONG.matchEntire(textForm) if (longMatch != null) { - return NbtLong.of(longMatch.groups[1]!!.value.toLong()) + return LongTag.valueOf(longMatch.groups[1]!!.value.toLong()) } val shortMatch = Patterns.SHORT.matchEntire(textForm) if (shortMatch != null) { - return NbtShort.of(shortMatch.groups[1]!!.value.toShort()) + return ShortTag.valueOf(shortMatch.groups[1]!!.value.toShort()) } val integerMatch = Patterns.INTEGER.matchEntire(textForm) if (integerMatch != null) { - return NbtInt.of(integerMatch.groups[1]!!.value.toInt()) + return IntTag.valueOf(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()) + return DoubleTag.valueOf(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") } diff --git a/src/main/kotlin/util/LegacyTagWriter.kt b/src/main/kotlin/util/LegacyTagWriter.kt new file mode 100644 index 0000000..eb755c4 --- /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.CollectionTag +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.DoubleTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.EndTag +import net.minecraft.nbt.FloatTag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.LongTag +import net.minecraft.nbt.ShortTag +import net.minecraft.nbt.StringTag +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.SIMPLE_NAME + +class LegacyTagWriter(val compact: Boolean) { + companion object { + fun stringify(nbt: Tag, compact: Boolean): String { + return LegacyTagWriter(compact).also { it.writeElement(nbt) } + .stringWriter.toString() + } + + fun Tag.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: Tag) { + when (nbt) { + is IntTag -> stringWriter.append(nbt.value.toString()) + is StringTag -> stringWriter.append(escapeString(nbt.value)) + is FloatTag -> stringWriter.append(nbt.value).append('F') + is DoubleTag -> stringWriter.append(nbt.value).append('D') + is ByteTag -> stringWriter.append(nbt.value).append('B') + is LongTag -> stringWriter.append(nbt.value).append('L') + is ShortTag -> stringWriter.append(nbt.value).append('S') + is CompoundTag -> writeCompound(nbt) + is EndTag -> {} + is CollectionTag -> writeArray(nbt) + } + } + + fun writeArray(nbt: CollectionTag) { + 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: CompoundTag) { + 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 index 4bc8704..d3a7ac2 100644 --- a/src/main/kotlin/util/LoadResource.kt +++ b/src/main/kotlin/util/LoadResource.kt @@ -4,17 +4,17 @@ 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 net.minecraft.resources.ResourceLocation import moe.nea.firmament.repo.RepoDownloadManager -fun Identifier.openFirmamentResource(): InputStream { +fun ResourceLocation.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 + return resource.open() } diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index 215d2a8..e70c382 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -1,37 +1,49 @@ package moe.nea.firmament.util import io.github.moulberry.repo.data.Coordinate +import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent import java.util.concurrent.ConcurrentLinkedQueue -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.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.network.packet.c2s.play.CommandExecutionC2SPacket -import net.minecraft.registry.BuiltinRegistries -import net.minecraft.registry.RegistryKeys -import net.minecraft.registry.RegistryWrapper -import net.minecraft.resource.ReloadableResourceManagerImpl -import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos -import net.minecraft.world.World +import kotlin.jvm.optionals.getOrNull +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.Gui +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.player.LocalPlayer +import net.minecraft.client.renderer.GameRenderer +import net.minecraft.client.renderer.LevelRenderer +import net.minecraft.client.renderer.entity.ItemRenderer +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.Entity +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.NbtOps +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket +import net.minecraft.data.registries.VanillaRegistries +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.RegistryOps +import net.minecraft.core.HolderLookup +import net.minecraft.server.packs.resources.ReloadableResourceManager +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.Util +import net.minecraft.core.BlockPos +import net.minecraft.world.level.Level +import moe.nea.firmament.Firmament import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.util.mc.TolerantRegistriesOps object MC { - private val messageQueue = ConcurrentLinkedQueue<Text>() + private val messageQueue = ConcurrentLinkedQueue<Component>() init { TickEvent.subscribe("MC:push") { - if (inGameHud.chatHud != null && world != null) + if (inGameHud.chat != null && world != null) while (true) { - inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) + inGameHud.chat.addMessage(messageQueue.poll() ?: break) } while (true) { (nextTickTodos.poll() ?: break).invoke() @@ -42,38 +54,42 @@ object MC { } } - fun sendChat(text: Text) { - if (instance.isOnThread && inGameHud.chatHud != null && world != null) - inGameHud.chatHud.addMessage(text) + fun sendChat(text: Component) { + if (TestUtil.isInTest) { + Firmament.logger.info("CHAT: ${text.string}") + return + } + if (instance.isSameThread && inGameHud.chat != null && world != null) + inGameHud.chat.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( + val nh = player?.connection ?: return + nh.send( + ServerboundChatCommandPacket( command, ) ) } fun sendServerChat(text: String) { - player?.networkHandler?.sendChatMessage(text) + player?.connection?.sendChat(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) + player?.connection?.sendCommand(command) } fun onMainThread(block: () -> Unit) { - if (instance.isOnThread) + if (instance.isSameThread) block() else - instance.send(block) + instance.schedule(block) } private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>() @@ -82,39 +98,62 @@ object MC { } - inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) + inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManager) inline val itemRenderer: ItemRenderer get() = instance.itemRenderer - inline val worldRenderer: WorldRenderer get() = instance.worldRenderer - 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 worldRenderer: LevelRenderer get() = instance.levelRenderer + inline val gameRenderer: GameRenderer get() = instance.gameRenderer + inline val networkHandler get() = player?.connection + inline val instance get() = Minecraft.getInstance() + inline val keyboard get() = instance.keyboardHandler + inline val interactionManager get() = instance.gameMode 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 inGameHud: Gui get() = instance.gui + inline val font get() = instance.font inline val soundManager get() = instance.soundManager - inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player } + inline val player: LocalPlayer? get() = TestUtil.unlessTesting { instance.player } inline val camera: Entity? get() = instance.cameraEntity - inline val guiAtlasManager get() = instance.guiAtlasManager - inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } - inline val playerName: String? get() = player?.name?.unformattedString + inline val stackInHand: ItemStack get() = player?.mainHandItem ?: ItemStack.EMPTY + inline val world: ClientLevel? get() = TestUtil.unlessTesting { instance.level } + inline val playerName: String get() = player?.name?.unformattedString ?: MC.instance.user.name inline var screen: Screen? - get() = TestUtil.unlessTesting { instance.currentScreen } + get() = TestUtil.unlessTesting { instance.screen } set(value) = instance.setScreen(value) val screenName get() = screen?.title?.unformattedString?.trim() - inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> + inline val handledScreen: AbstractContainerScreen<*>? get() = instance.screen as? AbstractContainerScreen<*> inline val window get() = instance.window - inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager - val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() } + inline val currentRegistries: HolderLookup.Provider? get() = world?.registryAccess() + val defaultRegistries: HolderLookup.Provider by lazy { VanillaRegistries.createLookup() } + val defaultRegistryNbtOps by lazy { RegistryOps.create(NbtOps.INSTANCE, defaultRegistries) } inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries - val defaultItems: RegistryWrapper.Impl<Item> by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) } - var lastWorld: World? = null + val currentOrDefaultRegistryNbtOps get() = TolerantRegistriesOps(NbtOps.INSTANCE, currentOrDefaultRegistries) + val defaultItems: HolderLookup.RegistryLookup<Item> by lazy { defaultRegistries.lookupOrThrow(Registries.ITEM) } + var currentTick = 0 + var lastWorld: Level? = null get() { field = world ?: field return field } private set + + val currentMoulConfigContext + get() = (screen as? MoulConfigScreenComponent)?.guiContext + + fun openUrl(uri: String) { + Util.getPlatform().openUri(uri) + } + + fun <T> unsafeGetRegistryEntry(registry: ResourceKey<out Registry<T>>, identifier: ResourceLocation) = + unsafeGetRegistryEntry(ResourceKey.create(registry, identifier)) + + + fun <T> unsafeGetRegistryEntry(registryKey: ResourceKey<T>): T? { + return currentOrDefaultRegistries + .lookupOrThrow(registryKey.registryKey()) + .get(registryKey) + .getOrNull() + ?.value() + } } diff --git a/src/main/kotlin/util/MinecraftDispatcher.kt b/src/main/kotlin/util/MinecraftDispatcher.kt index d1f22a9..3e23f54 100644 --- a/src/main/kotlin/util/MinecraftDispatcher.kt +++ b/src/main/kotlin/util/MinecraftDispatcher.kt @@ -3,6 +3,6 @@ package moe.nea.firmament.util import kotlinx.coroutines.asCoroutineDispatcher -import net.minecraft.client.MinecraftClient +import net.minecraft.client.Minecraft -val MinecraftDispatcher by lazy { MinecraftClient.getInstance().asCoroutineDispatcher() } +val MinecraftDispatcher by lazy { Minecraft.getInstance().asCoroutineDispatcher() } diff --git a/src/main/kotlin/util/MoulConfigFragment.kt b/src/main/kotlin/util/MoulConfigFragment.kt index 36132cd..200b780 100644 --- a/src/main/kotlin/util/MoulConfigFragment.kt +++ b/src/main/kotlin/util/MoulConfigFragment.kt @@ -1,44 +1,43 @@ - - 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 io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent import me.shedaniel.math.Point -import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component 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.doDrawTooltip() - } - - override fun close() { - dismiss() - } + context: GuiContext, + val position: Point, + val dismiss: () -> Unit +) : MoulConfigScreenComponent(Component.empty(), context, null) { + init { + this.init(MC.instance, MC.screen!!.width, MC.screen!!.height) + } + + override fun createContext(drawContext: GuiGraphics?): GuiImmediateContext { + val oldContext = super.createContext(drawContext) + return oldContext.translated( + position.x, + position.y, + guiContext.root.width, + guiContext.root.height, + ) + } + + + override fun render(drawContext: GuiGraphics, i: Int, j: Int, f: Float) { + val ctx = createContext(drawContext) + val m = drawContext.pose() + m.pushMatrix() + m.translate(position.x.toFloat(), position.y.toFloat()) + guiContext.root.render(ctx) + m.popMatrix() + ctx.renderContext.renderExtraLayers() + } + + override fun onClose() { + dismiss() + } } diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt index 62bf3dd..0c65e69 100644 --- a/src/main/kotlin/util/MoulConfigUtils.kt +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -4,13 +4,13 @@ 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.platform.MoulConfigRenderContext +import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent import io.github.notenoughupdates.moulconfig.xml.ChildCount import io.github.notenoughupdates.moulconfig.xml.XMLContext import io.github.notenoughupdates.moulconfig.xml.XMLGuiLoader @@ -23,8 +23,11 @@ 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.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import com.mojang.blaze3d.platform.InputConstants +import me.shedaniel.math.Rectangle +import net.minecraft.network.chat.Component import moe.nea.firmament.gui.BarComponent import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.gui.FirmHoverComponent @@ -34,6 +37,21 @@ import moe.nea.firmament.gui.TickComponent import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext object MoulConfigUtils { + @JvmStatic + fun main(args: Array<out String>) { + generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS) + generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl) + File("wrapper.xsd").writeText( + """ +<?xml version="1.0" encoding="UTF-8" ?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/> + <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/> +</xs:schema> + """.trimIndent() + ) + } + val firmUrl = "http://firmament.nea.moe/moulconfig" val universe = XMLUniverse.getDefaultUniverse().also { uni -> uni.registerMapper(java.awt.Color::class.java) { @@ -80,9 +98,11 @@ object MoulConfigUtils { override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent { return FirmHoverComponent( context.getChildFragment(element), - context.getPropertyFromAttribute(element, - QName("lines"), - List::class.java) as Supplier<List<String>>, + context.getPropertyFromAttribute( + element, + QName("lines"), + List::class.java + ) as Supplier<List<String>>, context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds), ) } @@ -178,10 +198,8 @@ object MoulConfigUtils { uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> { override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent { return FixedComponent( - context.getPropertyFromAttribute(element, QName("width"), Int::class.java) - ?: error("Requires width specified"), - context.getPropertyFromAttribute(element, QName("height"), Int::class.java) - ?: error("Requires height specified"), + context.getPropertyFromAttribute(element, QName("width"), Int::class.java), + context.getPropertyFromAttribute(element, QName("height"), Int::class.java), context.getChildFragment(element) ) } @@ -195,7 +213,7 @@ object MoulConfigUtils { } override fun getAttributeNames(): Map<String, Boolean> { - return mapOf("width" to true, "height" to true) + return mapOf("width" to false, "height" to false) } }) } @@ -209,29 +227,21 @@ object MoulConfigUtils { generator.dumpToFile(file) } - @JvmStatic - fun main(args: Array<out String>) { - generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS) - generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl) - File("wrapper.xsd").writeText(""" -<?xml version="1.0" encoding="UTF-8" ?> -<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/> - <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/> -</xs:schema> - """.trimIndent()) - } - - fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen { - return object : GuiComponentWrapper(loadGui(name, bindTo)) { - override fun close() { - if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { - client!!.setScreen(parent) + fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen { + return object : MoulConfigScreenComponent(Component.empty(), guiContext, null) { + override fun onClose() { + if (guiContext.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { + minecraft!!.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 }) @@ -256,8 +266,18 @@ object MoulConfigUtils { h: Int, keyboardEvent: KeyboardEvent ): Boolean { - val immContext = createInPlaceFullContext(null, IMinecraft.instance.mouseX, IMinecraft.instance.mouseY) - return component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h)) + 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 == InputConstants.KEY_ESCAPE + ) { + component.context.setFocusedElement(null) + } + return true + } + return false } fun clickMCComponentInPlace( @@ -273,20 +293,40 @@ object MoulConfigUtils { 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()) + fun <T> createAndTranslateFullContext( + drawContext: GuiGraphics?, + mouseX: Number, mouseY: Number, + rectangle: Rectangle, + block: (GuiImmediateContext) -> T + ): T { + val ctx = createInPlaceFullContext(drawContext, mouseX, mouseY) + val pose = drawContext?.pose() + pose?.pushMatrix() + pose?.translate(rectangle.x.toFloat(), rectangle.y.toFloat()) + val result = block(ctx.translated(rectangle.x, rectangle.y, rectangle.width, rectangle.height)) + pose?.popMatrix() + return result + } + + fun createInPlaceFullContext(drawContext: GuiGraphics?, mouseX: Number, mouseY: Number): GuiImmediateContext { + ErrorUtil.softCheck( + "created moulconfig context with pre-existing translations.", + drawContext?.isUntranslatedGuiDrawContext() != false + ) + val context = drawContext?.let(::MoulConfigRenderContext) + ?: IMinecraft.INSTANCE.provideTopLevelRenderContext() + val immContext = GuiImmediateContext( + context, + 0, 0, 0, 0, + mouseX.toInt(), mouseY.toInt(), + mouseX.toInt(), mouseY.toInt(), + mouseX.toFloat(), + mouseY.toFloat() + ) return immContext } - fun DrawContext.drawMCComponentInPlace( + fun GuiGraphics.drawMCComponentInPlace( component: GuiComponent, x: Int, y: Int, @@ -296,10 +336,10 @@ object MoulConfigUtils { mouseY: Int ) { val immContext = createInPlaceFullContext(this, mouseX, mouseY) - matrices.push() - matrices.translate(x.toFloat(), y.toFloat(), 0F) + pose().pushMatrix() + pose().translate(x.toFloat(), y.toFloat()) component.render(immContext.translated(x, y, w, h)) - matrices.pop() + pose().popMatrix() } diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt index e785ff6..b3f162b 100644 --- a/src/main/kotlin/util/SBData.kt +++ b/src/main/kotlin/util/SBData.kt @@ -18,7 +18,16 @@ object SBData { "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]", "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]", ) + + val NULL_UUID = UUID(0L, 0L) + val profileIdOrNil get() = profileId ?: NULL_UUID + 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 @@ -26,8 +35,18 @@ object SBData { 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) + + /** + * Check if the player is currently on skyblock. + * + * Nota bene: We don't generally disable features outside of SkyBlock unless they could lead to bans. + */ val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK" var profileIdCommandDebounce = TimeMark.farPast() fun init() { @@ -37,18 +56,22 @@ object SBData { 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") + MC.sendCommand("profileid") } } AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event -> diff --git a/src/main/kotlin/util/ScoreboardUtil.kt b/src/main/kotlin/util/ScoreboardUtil.kt index 4311971..d94eb54 100644 --- a/src/main/kotlin/util/ScoreboardUtil.kt +++ b/src/main/kotlin/util/ScoreboardUtil.kt @@ -1,45 +1,55 @@ +package moe.nea.firmament.util +import java.util.Optional +import net.minecraft.client.gui.Gui +import net.minecraft.world.scores.DisplaySlot +import net.minecraft.world.scores.PlayerTeam +import net.minecraft.network.chat.FormattedText +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent -package moe.nea.firmament.util +object ScoreboardUtil { + var scoreboardLines: List<Component> = listOf() + var simplifiedScoreboardLines: List<String> = listOf() -import java.util.* -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 + @Subscribe + fun onTick(event: TickEvent) { + scoreboardLines = getScoreboardLinesUncached() + simplifiedScoreboardLines = scoreboardLines.map { it.unformattedString } + } -fun getScoreboardLines(): List<Text> { - 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) - } + private fun getScoreboardLinesUncached(): List<Component> { + val scoreboard = MC.instance.level?.scoreboard ?: return listOf() + val activeObjective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR) ?: return listOf() + return scoreboard.listPlayerScores(activeObjective) + .filter { !it.isHidden() } + .sortedWith(Gui.SCORE_DISPLAY_ORDER) + .take(15).map { + val team = scoreboard.getPlayersTeam(it.owner) + val text = it.ownerName() + PlayerTeam.formatNameForTeam(team, text) + } + } } - -fun Text.formattedString(): String { - val sb = StringBuilder() - visit(StringVisitable.StyledVisitor<Unit> { 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(), "") +fun Component.formattedString(): String { + val sb = StringBuilder() + visit(FormattedText.StyledContentConsumer<Unit> { style, string -> + val c = ChatFormatting.getByName(style.color?.serialize()) + if (c != null) { + sb.append("§${c.char}") + } + 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 index 99d77fb..98875e0 100644 --- a/src/main/kotlin/util/ScreenUtil.kt +++ b/src/main/kotlin/util/ScreenUtil.kt @@ -3,8 +3,8 @@ 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 net.minecraft.client.Minecraft +import net.minecraft.client.gui.screens.Screen import moe.nea.firmament.Firmament object ScreenUtil { @@ -12,11 +12,11 @@ object ScreenUtil { ClientTickEvents.START_CLIENT_TICK.register(::onTick) } - private fun onTick(minecraft: MinecraftClient) { + private fun onTick(minecraft: Minecraft) { if (nextOpenedGui != null) { val p = minecraft.player - if (p?.currentScreenHandler != null) { - p.closeHandledScreen() + if (p?.containerMenu != null) { + p.closeContainer() } minecraft.setScreen(nextOpenedGui) nextOpenedGui = null diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt index f15cadb..0fa6376 100644 --- a/src/main/kotlin/util/SkyBlockIsland.kt +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.util import kotlinx.serialization.KSerializer @@ -13,32 +12,44 @@ import moe.nea.firmament.repo.RepoManager @Serializable(with = SkyBlockIsland.Serializer::class) class SkyBlockIsland private constructor( - val locrawMode: String, + val locrawMode: String, ) { - object Serializer : KSerializer<SkyBlockIsland> { - 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<String, SkyBlockIsland>() - fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) - val HUB = forMode("hub") - val PRIVATE_ISLAND = forMode("dynamic") - val RIFT = forMode("rift") - val MINESHAFT = forMode("mineshaft") - val GARDEN = forMode("garden") - } - - val userFriendlyName - get() = RepoManager.neuRepo.constants.islands.areaNames - .getOrDefault(locrawMode, locrawMode) + object Serializer : KSerializer<SkyBlockIsland> { + 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<String, SkyBlockIsland>() + 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 GALATEA = forMode("foraging_2") + } + + val hasCustomMining + get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false + val isModernServer + get() = this == GALATEA + + 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 index 631b444..0d21559 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -6,50 +6,69 @@ 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 net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.CustomData +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.repo.ExpLadders +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.ItemNameLookup +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 +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.unsafeNbt +import moe.nea.firmament.util.skyblock.ScreenIdentification +import moe.nea.firmament.util.skyblock.ScreenType /** * 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`, + * 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) { +value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> { val identifier - get() = Identifier.of("skyblockitem", - neuItem.lowercase().replace(";", "__") - .replace(":", "___") - .replace(illlegalPathRegex) { - it.value.toCharArray() - .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') } - }) + get() = ResourceLocation.fromNamespaceAndPath( + "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. + * 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. @@ -57,11 +76,10 @@ value class SkyblockId(val neuItem: String) { @JvmInline @Serializable value class BazaarStock(val bazaarId: String) { - fun toRepoId(): SkyblockId { - bazaarEnchantmentRegex.matchEntire(bazaarId)?.let { - return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}") + companion object { + fun fromSkyBlockId(skyblockId: SkyblockId): BazaarStock { + return BazaarStock(RepoManager.neuRepo.constants.bazaarStocks.getBazaarStockOrDefault(skyblockId.neuItem)) } - return SkyblockId(bazaarId.replace(":", "-")) } } @@ -73,14 +91,16 @@ value class SkyblockId(val neuItem: String) { 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<in RegistryByteBuf, SkyblockId> = - PacketCodecs.STRING.xmap({ SkyblockId(it) }, { it.neuItem }) + val PACKET_CODEC: StreamCodec<in RegistryFriendlyByteBuf, SkyblockId> = + ByteBufCodecs.STRING_UTF8.map({ 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() @@ -99,34 +119,68 @@ data class HypixelPetInfo( val exp: Double = 0.0, val candyUsed: Int = 0, val uuid: UUID? = null, - val active: Boolean = false, + 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 +var ItemStack.extraAttributes: CompoundTag set(value) { - set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(value)) + set(DataComponents.CUSTOM_DATA, CustomData.of(value)) } get() { - val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run { - val component = NbtComponent.of(NbtCompound()) - set(DataComponentTypes.CUSTOM_DATA, component) + val customData = get(DataComponents.CUSTOM_DATA) ?: run { + val component = CustomData.of(CompoundTag()) + set(DataComponents.CUSTOM_DATA, component) component } - return customData.nbt + return customData.unsafeNbt } -val ItemStack.skyblockUUIDString: String? - get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() } +fun ItemStack.modifyExtraAttributes(block: (CompoundTag) -> Unit) { + val baseNbt = get(DataComponents.CUSTOM_DATA)?.copyTag() ?: CompoundTag() + block(baseNbt) + set(DataComponents.CUSTOM_DATA, CustomData.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 { + parseCaseInsensitive() + appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NOT_NEGATIVE) + appendLiteral("/") + appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE) + appendLiteral("/") + appendValueReduced(ChronoField.YEAR, 2, 2, 1950) + appendLiteral(" ") + appendValue(ChronoField.CLOCK_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) } + get() = skyBlockUUIDString?.let { UUID.fromString(it) } private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") { val jsonString = it.extraAttributes.getString("petInfo") + .getOrNull() if (jsonString.isNullOrBlank()) return@memoize Optional.empty() ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) @@ -135,8 +189,8 @@ private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo> } fun ItemStack.getUpgradeStars(): Int { - return extraAttributes.getInt("upgrade_level").takeIf { it > 0 } - ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 } + return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 } + ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 } ?: 0 } @@ -145,7 +199,7 @@ fun ItemStack.getUpgradeStars(): Int { value class ReforgeId(val id: String) fun ItemStack.getReforgeId(): ReforgeId? { - return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId) + return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId) } val ItemStack.petData: HypixelPetInfo? @@ -157,11 +211,52 @@ fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack { return this } +private val STORED_REGEX = "Stored: ($SHORT_NUMBER_FORMAT)/.+".toPattern() +private val COMPOST_REGEX = "Compost Available: ($SHORT_NUMBER_FORMAT)".toPattern() +private val GEMSTONE_SACK_REGEX = " Amount: ($SHORT_NUMBER_FORMAT)".toPattern() +private val AMOUNT_REGEX = ".*(?:Offer amount|Amount|Order amount): ($SHORT_NUMBER_FORMAT)x".toPattern() +fun ItemStack.getLogicalStackSize(): Long { + return loreAccordingToNbt.firstNotNullOfOrNull { + val string = it.unformattedString + GEMSTONE_SACK_REGEX.useMatch(string) { + parseShortNumber(group(1)).toLong() + } ?: STORED_REGEX.useMatch(string) { + parseShortNumber(group(1)).toLong() + } ?: AMOUNT_REGEX.useMatch(string) { + parseShortNumber(group(1)).toLong() + } ?: COMPOST_REGEX.useMatch(string) { + parseShortNumber(group(1)).toLong() + } + } ?: count.toLong() +} + +val ItemStack.rawSkyBlockId: String? get() = extraAttributes.getString("id").getOrNull() + +fun ItemStack.guessContextualSkyBlockId(): SkyblockId? { + val screen = MC.screen + val screenType = ScreenIdentification.getType(screen) + if (screenType == ScreenType.BAZAAR_ANY || screenType == ScreenType.DYE_COMPENDIUM) { + val name = displayNameAccordingToNbt.unformattedString + .replaceFirst("SELL ", "") + .replaceFirst("BUY ", "") + if (item == Items.ENCHANTED_BOOK) { + return RepoManager.enchantedBookCache.byName[name] + } + return ItemNameLookup.guessItemByName(name, false) + } + if (screen != null && (screenType == ScreenType.EXPERIMENTATION_RNG_METER || screenType == ScreenType.SUPER_PAIRS || screenType == ScreenType.ENCHANTMENT_GUIDE)) { + val name = displayNameAccordingToNbt.unformattedString + return RepoManager.enchantedBookCache.byName[name] + ?: ItemNameLookup.guessItemByName(name, false) + } + return null +} + val ItemStack.skyBlockId: SkyblockId? get() { - return when (val id = extraAttributes.getString("id")) { - "" -> { - null + return when (val id = rawSkyBlockId) { + "", null -> { + guessContextualSkyBlockId() } "PET" -> { @@ -170,25 +265,68 @@ val ItemStack.skyBlockId: SkyblockId? "RUNE", "UNIQUE_RUNE" -> { val runeData = extraAttributes.getCompound("runes") - val runeKind = runeData.keys.singleOrNull() + .getOrNull() + val runeKind = runeData?.keySet()?.singleOrNull() if (runeKind == null) SkyblockId("RUNE") - else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind)}") + else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind).getOrNull()}") } "ABICASE" -> { - SkyblockId("ABICASE_${extraAttributes.getString("model").uppercase()}") + SkyblockId("ABICASE_${extraAttributes.getString("model").getOrNull()?.uppercase()}") } "ENCHANTED_BOOK" -> { val enchantmentData = extraAttributes.getCompound("enchantments") - val enchantName = enchantmentData.keys.singleOrNull() + .getOrNull() + val enchantName = enchantmentData?.keySet()?.singleOrNull() if (enchantName == null) SkyblockId("ENCHANTED_BOOK") - else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName)}") + else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}") + } + + "ATTRIBUTE_SHARD" -> { + val attributeData = extraAttributes.getCompound("attributes").getOrNull() + val attributeName = attributeData?.keySet()?.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()}") } - // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION else -> { - SkyblockId(id) + SkyblockId(id.replace(":", "-")) } } } diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt index 68e161a..50c5367 100644 --- a/src/main/kotlin/util/StringUtil.kt +++ b/src/main/kotlin/util/StringUtil.kt @@ -5,10 +5,18 @@ object StringUtil { return splitToSequence(" ") // TODO: better boundaries } + fun String.camelWords(): Sequence<String> { + 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<String>.unwords() = joinToString(" ") fun nextLexicographicStringOfSameLength(string: String): String { val next = StringBuilder(string) diff --git a/src/main/kotlin/util/TemplateUtil.kt b/src/main/kotlin/util/TemplateUtil.kt index f4ff37c..44d9ccd 100644 --- a/src/main/kotlin/util/TemplateUtil.kt +++ b/src/main/kotlin/util/TemplateUtil.kt @@ -2,10 +2,9 @@ package moe.nea.firmament.util -import java.util.* +import java.util.Base64 import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.json.Json import kotlinx.serialization.serializer import moe.nea.firmament.Firmament diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt index 45e3dde..da8ba38 100644 --- a/src/main/kotlin/util/TestUtil.kt +++ b/src/main/kotlin/util/TestUtil.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.util object TestUtil { inline fun <T> 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 index 4a076ac..112a727 100644 --- a/src/main/kotlin/util/TimeMark.kt +++ b/src/main/kotlin/util/TimeMark.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.util import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit class TimeMark private constructor(private val timeMark: Long) : Comparable<TimeMark> { fun passedTime() = @@ -50,3 +51,7 @@ class TimeMark private constructor(private val timeMark: Long) : Comparable<Time return this.timeMark.compareTo(other.timeMark) } } + +fun Duration.toTicks(): Long { + return toLong(DurationUnit.MILLISECONDS) / 50 +} diff --git a/src/main/kotlin/util/WarpUtil.kt b/src/main/kotlin/util/WarpUtil.kt index f733af7..3008592 100644 --- a/src/main/kotlin/util/WarpUtil.kt +++ b/src/main/kotlin/util/WarpUtil.kt @@ -6,91 +6,94 @@ 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 net.minecraft.network.chat.Component +import net.minecraft.core.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.Config import moe.nea.firmament.util.data.ProfileSpecificDataHolder object WarpUtil { - val warps: Sequence<Islands.Warp> get() = RepoManager.neuRepo.constants.islands.warps - .asSequence() - .filter { it.warp !in ignoredWarps } + val warps: Sequence<Islands.Warp> + get() = RepoManager.neuRepo.constants.islands.warps + .asSequence() + .filter { it.warp !in ignoredWarps } - val ignoredWarps = setOf("carnival", "") + val ignoredWarps = setOf("carnival", "") - @Serializable - data class Data( - val excludedWarps: MutableSet<String> = mutableSetOf(), - ) + @Serializable + data class Data( + val excludedWarps: MutableSet<String> = mutableSetOf(), + ) - object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "warp-util", ::Data) + @Config + object DConfig : ProfileSpecificDataHolder<Data>(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 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 - } + 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}") - } + fun teleportToNearestWarp(island: SkyBlockIsland, pos: Position) { + val nearestWarp = findNearestWarp(island, pos) + if (nearestWarp == null) { + MC.sendChat(Component.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).position, nearestWarp)) + ) { + MC.sendChat(Component.translatable("firmament.warp-util.already-close", nearestWarp.warp)) + return + } + MC.sendChat(Component.translatable("firmament.warp-util.attempting-to-warp", nearestWarp.warp)) + lastWarpAttempt = TimeMark.now() + lastAttemptedWarp = nearestWarp.warp + MC.sendCommand("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")) - } - } - } + @Subscribe + fun clearUnlockedWarpsCommand(event: CommandEvent.SubCommand) { + event.subcommand("clearwarps") { + thenExecute { + DConfig.data?.excludedWarps?.clear() + DConfig.markDirty() + source.sendFeedback(Component.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() - } - } - } + 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(Component.translatableEscape("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 index 37acfd9..109de94 100644 --- a/src/main/kotlin/util/accessors/GetRectangle.kt +++ b/src/main/kotlin/util/accessors/GetRectangle.kt @@ -3,11 +3,11 @@ package moe.nea.firmament.util.accessors import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen import moe.nea.firmament.mixins.accessor.AccessorHandledScreen -import net.minecraft.client.gui.screen.ingame.HandledScreen -fun HandledScreen<*>.getRectangle(): Rectangle { - this as AccessorHandledScreen +fun AbstractContainerScreen<*>.getProperRectangle(): Rectangle { + this.castAccessor() return Rectangle( getX_Firmament(), getY_Firmament(), diff --git a/src/main/kotlin/util/accessors/castAccessor.kt b/src/main/kotlin/util/accessors/castAccessor.kt new file mode 100644 index 0000000..0ac85a2 --- /dev/null +++ b/src/main/kotlin/util/accessors/castAccessor.kt @@ -0,0 +1,16 @@ +@file:OptIn(ExperimentalContracts::class) + +package moe.nea.firmament.util.accessors + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen + + +inline fun AbstractContainerScreen<*>.castAccessor(): AccessorHandledScreen { + contract { + returns() implies (this@castAccessor is AccessorHandledScreen) + } + return this as AccessorHandledScreen +} diff --git a/src/main/kotlin/util/accessors/chathud.kt b/src/main/kotlin/util/accessors/chathud.kt index effac7d..7935ad4 100644 --- a/src/main/kotlin/util/accessors/chathud.kt +++ b/src/main/kotlin/util/accessors/chathud.kt @@ -1,8 +1,8 @@ package moe.nea.firmament.util.accessors -import net.minecraft.client.gui.hud.ChatHud -import net.minecraft.client.gui.hud.ChatHudLine +import net.minecraft.client.gui.components.ChatComponent +import net.minecraft.client.GuiMessage import moe.nea.firmament.mixins.accessor.AccessorChatHud -val ChatHud.messages: MutableList<ChatHudLine> +val ChatComponent.messages: MutableList<GuiMessage> 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<out Annotation>, + 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<Any>) + .map { unmap(it, comp, depth - 1) } as java.util.List<Any>) + .toArray(java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0) as Array<*>) + if (comp.isEnum) { + comp as Class<out Enum<*>> + 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<out Annotation>, 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<out Any?>? + ): 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 <T : Annotation> createProxy( + annotationClass: Class<T>, + 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/async/CompletableFutureExt.kt b/src/main/kotlin/util/async/CompletableFutureExt.kt new file mode 100644 index 0000000..5476371 --- /dev/null +++ b/src/main/kotlin/util/async/CompletableFutureExt.kt @@ -0,0 +1,6 @@ +package moe.nea.firmament.util.async + +import java.util.concurrent.CompletableFuture + + +fun CompletableFuture<*>.discard(): CompletableFuture<Void?> = thenRun { } diff --git a/src/main/kotlin/util/async/input.kt b/src/main/kotlin/util/async/input.kt index f22c595..65479e9 100644 --- a/src/main/kotlin/util/async/input.kt +++ b/src/main/kotlin/util/async/input.kt @@ -1,47 +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.screens.Screen import moe.nea.firmament.events.HandledScreenKeyPressedEvent -import moe.nea.firmament.keybindings.IKeyBinding +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.SavedKeyBinding +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<KeyInputContinuation>() - - 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) - } - } - } + data class KeyInputContinuation(val keybind: SavedKeyBinding, val onContinue: () -> Unit) + + private val activeContinuations = mutableListOf<KeyInputContinuation>() + + 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() - } +suspend fun waitForInput(keybind: SavedKeyBinding): 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<String> { cont -> + lateinit var screen: Screen + lateinit var text: GetSetter<String> + 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/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<Float>.centre get() = (endInclusive + start) / 2 + +fun ClosedFloatingPointRange<Float>.nonNegligibleSubSectionsAlignedWith( + interval: Float +): Iterable<Float> { + require(interval.isFinite()) + val range = this + return object : Iterable<Float> { + override fun iterator(): Iterator<Float> { + 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 index 38f9886..4a48c63 100644 --- a/src/main/kotlin/util/collections/WeakCache.kt +++ b/src/main/kotlin/util/collections/WeakCache.kt @@ -9,102 +9,108 @@ import moe.nea.firmament.features.debug.DebugLogger * 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. */ -class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) { - private val queue = object : ReferenceQueue<Key>() {} - private val map = mutableMapOf<Ref, Value>() - - val size: Int - get() { - clearOldReferences() - return map.size - } - - fun clearOldReferences() { - var successCount = 0 - var totalCount = 0 - while (true) { - val reference = queue.poll() ?: break - totalCount++ - if (map.remove(reference) != null) - successCount++ - } - if (totalCount > 0) - logger.log { "Cleared $successCount/$totalCount references from queue" } - } - - fun get(key: Key, extraData: ExtraKey): Value? { - clearOldReferences() - return map[Ref(key, extraData)] - } - - fun put(key: Key, extraData: ExtraKey, value: Value) { - clearOldReferences() - map[Ref(key, extraData)] = value - } - - fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value { - clearOldReferences() - return map.getOrPut(Ref(key, extraData)) { value(key, extraData) } - } - - fun clear() { - map.clear() - } - - init { - allInstances.add(this) - } - - companion object { - val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches") - private val logger = DebugLogger("WeakCache") - fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value): - CacheFunction.NoExtraData<Key, Value> { - return CacheFunction.NoExtraData(WeakCache(name), function) - } - - fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value): - CacheFunction.WithExtraData<Key, ExtraKey, Value> { - return CacheFunction.WithExtraData(WeakCache(name), function) - } - } - - inner class Ref( - weakInstance: Key, - val extraData: ExtraKey, - ) : WeakReference<Key>(weakInstance, queue) { - 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<Key : Any, Value : Any>( - override val cache: WeakCache<Key, Unit, Value>, - val wrapped: (Key) -> Value, - ) : CacheFunction, (Key) -> Value { - override fun invoke(p1: Key): Value { - return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) - } - } - - data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>( - override val cache: WeakCache<Key, ExtraKey, Value>, - val wrapped: (Key, ExtraKey) -> Value, - ) : CacheFunction, (Key, ExtraKey) -> Value { - override fun invoke(p1: Key, p2: ExtraKey): Value { - return cache.getOrPut(p1, p2, wrapped) - } - } - } +open class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) { + private val queue = object : ReferenceQueue<Key>() {} + private val map = mutableMapOf<Ref, Value>() + + 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<WeakCache<*, *, *>>("WeakCaches") + private val logger = DebugLogger("WeakCache") + fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value): + CacheFunction.NoExtraData<Key, Value> { + return CacheFunction.NoExtraData(WeakCache(name), function) + } + + fun <Key : Any, ExtraKey : Any, Value : Any> dontMemoize(name: String, function: (Key, ExtraKey) -> Value) = function + fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value): + CacheFunction.WithExtraData<Key, ExtraKey, Value> { + return CacheFunction.WithExtraData(WeakCache(name), function) + } + } + + open inner class Ref( + weakInstance: Key, + val extraData: ExtraKey, + ) : WeakReference<Key>(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<Key : Any, Value : Any>( + override val cache: WeakCache<Key, Unit, Value>, + val wrapped: (Key) -> Value, + ) : CacheFunction, (Key) -> Value { + override fun invoke(p1: Key): Value { + return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) + } + } + + data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>( + override val cache: WeakCache<Key, ExtraKey, Value>, + 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/colorconversion.kt b/src/main/kotlin/util/colorconversion.kt index d7a5dad..758e354 100644 --- a/src/main/kotlin/util/colorconversion.kt +++ b/src/main/kotlin/util/colorconversion.kt @@ -2,12 +2,12 @@ package moe.nea.firmament.util -import net.minecraft.text.TextColor -import net.minecraft.util.DyeColor +import net.minecraft.network.chat.TextColor +import net.minecraft.world.item.DyeColor fun DyeColor.toShedaniel(): me.shedaniel.math.Color = - me.shedaniel.math.Color.ofOpaque(this.signColor) + me.shedaniel.math.Color.ofOpaque(this.textColor) fun DyeColor.toTextColor(): TextColor = - TextColor.fromRgb(this.signColor) + TextColor.fromRgb(this.textColor) diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt index 6b60e87..d1073af 100644 --- a/src/main/kotlin/util/compatloader/CompatLoader.kt +++ b/src/main/kotlin/util/compatloader/CompatLoader.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KClass import kotlin.streams.asSequence import moe.nea.firmament.Firmament -abstract class CompatLoader<T : Any>(val kClass: Class<T>) { +open class CompatLoader<T : Any>(val kClass: Class<T>) { constructor(kClass: KClass<T>) : this(kClass.java) val loader: ServiceLoader<T> = ServiceLoader.load(kClass) 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 index c61c711..e565850 100644 --- a/src/main/kotlin/util/customgui/CoordRememberingSlot.kt +++ b/src/main/kotlin/util/customgui/CoordRememberingSlot.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.util.customgui -import net.minecraft.screen.slot.Slot +import net.minecraft.world.inventory.Slot interface CoordRememberingSlot { fun rememberCoords_firmament() diff --git a/src/main/kotlin/util/customgui/CustomGui.kt b/src/main/kotlin/util/customgui/CustomGui.kt index 35c60ac..f64bf4d 100644 --- a/src/main/kotlin/util/customgui/CustomGui.kt +++ b/src/main/kotlin/util/customgui/CustomGui.kt @@ -1,8 +1,11 @@ package moe.nea.firmament.util.customgui import me.shedaniel.math.Rectangle -import net.minecraft.client.gui.DrawContext -import net.minecraft.screen.slot.Slot +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.inventory.Slot import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HandledScreenPushREIEvent @@ -23,19 +26,19 @@ abstract class CustomGui { } open fun render( - drawContext: DrawContext, - delta: Float, - mouseX: Int, - mouseY: Int + drawContext: GuiGraphics, + delta: Float, + mouseX: Int, + mouseY: Int ) { } - open fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + open fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { return false } - open fun afterSlotRender(context: DrawContext, slot: Slot) {} - open fun beforeSlotRender(context: DrawContext, slot: Slot) {} + open fun afterSlotRender(context: GuiGraphics, slot: Slot) {} + open fun beforeSlotRender(context: GuiGraphics, slot: Slot) {} open fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean { return false } @@ -69,23 +72,23 @@ abstract class CustomGui { return true } - open fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + open fun mouseReleased(click: MouseButtonEvent): Boolean { return false } - open fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + open fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { return false } - open fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + open fun keyPressed(input: KeyEvent): Boolean { return false } - open fun charTyped(chr: Char, modifiers: Int): Boolean { + open fun charTyped(input: CharacterEvent): Boolean { return false } - open fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + open fun keyReleased(input: KeyEvent): Boolean { return false } } diff --git a/src/main/kotlin/util/customgui/HasCustomGui.kt b/src/main/kotlin/util/customgui/HasCustomGui.kt index edead2e..7182979 100644 --- a/src/main/kotlin/util/customgui/HasCustomGui.kt +++ b/src/main/kotlin/util/customgui/HasCustomGui.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.util.customgui -import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen @Suppress("FunctionName") interface HasCustomGui { @@ -9,7 +9,7 @@ interface HasCustomGui { fun setCustomGui_Firmament(gui: CustomGui?) } -var <T : HandledScreen<*>> T.customGui: CustomGui? +var <T : AbstractContainerScreen<*>> 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/Config.kt b/src/main/kotlin/util/data/Config.kt new file mode 100644 index 0000000..41de039 --- /dev/null +++ b/src/main/kotlin/util/data/Config.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.util.data + +import moe.nea.firmament.util.compatloader.CompatLoader + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class Config(val prefix: String = "") + + +interface IConfigProvider { + val configs: List<IDataHolder<*>> + companion object { + val providers = CompatLoader(IConfigProvider::class) + } +} diff --git a/src/main/kotlin/util/data/DataHolder.kt b/src/main/kotlin/util/data/DataHolder.kt index 21a6014..c138d78 100644 --- a/src/main/kotlin/util/data/DataHolder.kt +++ b/src/main/kotlin/util/data/DataHolder.kt @@ -1,62 +1,13 @@ - - 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 +import moe.nea.firmament.gui.config.storage.ConfigStorageClass abstract class DataHolder<T>( - val serializer: KSerializer<T>, - val name: String, - val default: () -> T -) : IDataHolder<T> { - - - 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) - } - + serializer: KSerializer<T>, + name: String, + default: () -> T +) : GenericConfig<T>(name, serializer, default) { + override val storageClass: ConfigStorageClass + get() = ConfigStorageClass.STORAGE } diff --git a/src/main/kotlin/util/data/IDataHolder.kt b/src/main/kotlin/util/data/IDataHolder.kt index 1e9ba98..3229011 100644 --- a/src/main/kotlin/util/data/IDataHolder.kt +++ b/src/main/kotlin/util/data/IDataHolder.kt @@ -1,71 +1,117 @@ 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 java.util.UUID +import java.util.concurrent.CompletableFuture +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject import moe.nea.firmament.Firmament -import moe.nea.firmament.events.ScreenChangeEvent -import moe.nea.firmament.util.MC +import moe.nea.firmament.gui.config.storage.ConfigStorageClass +import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader +import moe.nea.firmament.util.SBData -interface IDataHolder<T> { - companion object { - internal var badLoads: MutableList<String> = CopyOnWriteArrayList() - private val allConfigs: MutableMap<KClass<out IDataHolder<*>>, IDataHolder<*>> = mutableMapOf() - private val dirty: MutableSet<KClass<out IDataHolder<*>>> = mutableSetOf() +sealed class IDataHolder<T> { + fun markDirty(future: CompletableFuture<Void?>? = null) { + FirmamentConfigLoader.markDirty(this, future) + } - internal fun <T : IDataHolder<K>, K> putDataHolder(kClass: KClass<T>, inst: IDataHolder<K>) { - allConfigs[kClass] = inst - } + init { + require(this.javaClass.getAnnotation(Config::class.java) != null) + } + + abstract fun keys(): Collection<T> + abstract fun saveTo(key: T): JsonObject + abstract fun loadFrom(key: T, jsonObject: JsonObject) + abstract fun explicitDefaultLoad() + abstract fun clear() + abstract val storageClass: ConfigStorageClass +} + +open class ProfileKeyedConfig<T>( + val prefix: String, + val serializer: KSerializer<T>, + val default: () -> T & Any, +) : IDataHolder<UUID>() { + + override val storageClass: ConfigStorageClass + get() = ConfigStorageClass.PROFILE + private var _data: MutableMap<UUID, T>? = null - fun <T : IDataHolder<K>, K> markDirty(kClass: KClass<T>) { - if (kClass !in allConfigs) { - Firmament.logger.error("Tried to markDirty '${kClass.qualifiedName}', which isn't registered as 'IConfigHolder'") - return - } - dirty.add(kClass) + val data: T & Any + get() { + val map = _data ?: error("Config $this not loaded — forgot to register?") + map[SBData.profileIdOrNil]?.let { return it } + val newValue = default() + map[SBData.profileIdOrNil] = newValue + return newValue } - 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() - } + override fun keys(): Collection<UUID> { + return _data!!.keys + } + + override fun saveTo(key: UUID): JsonObject { + val d = _data!! + return buildJsonObject { + put(prefix, Firmament.json.encodeToJsonElement(serializer, d[key] ?: return@buildJsonObject)) } + } - 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() - } + override fun loadFrom(key: UUID, jsonObject: JsonObject) { + var map = _data + if (map == null) { + map = mutableMapOf() + _data = map } + map[key] = + jsonObject[prefix] + ?.let { + Firmament.json.decodeFromJsonElement(serializer, it) + } ?: default() + } - fun registerEvents() { - ScreenChangeEvent.subscribe("IDataHolder:saveOnScreenChange") { event -> - performSaves() - warnForResetConfigs() - } - ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { - performSaves() - }) + override fun explicitDefaultLoad() { + _data = mutableMapOf() + } + + override fun clear() { + _data = null + } +} + +abstract class GenericConfig<T>( + val prefix: String, + val serializer: KSerializer<T>, + val default: () -> T, +) : IDataHolder<Unit>() { + + private var _data: T? = null + + val data get() = _data ?: error("Config $this not loaded — forgot to register?") + + override fun keys(): Collection<Unit> { + return listOf(Unit) + } + + override fun explicitDefaultLoad() { + _data = default() + } + + open fun onLoad() { + } + + override fun saveTo(key: Unit): JsonObject { + return buildJsonObject { + put(prefix, Firmament.json.encodeToJsonElement(serializer, data)) } + } + override fun loadFrom(key: Unit, jsonObject: JsonObject) { + _data = jsonObject[prefix]?.let { Firmament.json.decodeFromJsonElement(serializer, it) } ?: default() + onLoad() } - val data: T - fun save() - fun markDirty() - fun load() + override fun clear() { + _data = null + } } diff --git a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt index 1cd4f22..853ba7d 100644 --- a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt +++ b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt @@ -1,84 +1,9 @@ - - 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<S>( - private val dataSerializer: KSerializer<S>, - val configName: String, - private val configDefault: () -> S -) : IDataHolder<S?> { - - var allConfigs: MutableMap<UUID, S> - - 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<UUID, S> { - 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() - } - -} + dataSerializer: KSerializer<S>, + configName: String, + configDefault: () -> S & Any +) : ProfileKeyedConfig<S>(configName, dataSerializer, configDefault) diff --git a/src/main/kotlin/util/json/BlockPosSerializer.kt b/src/main/kotlin/util/json/BlockPosSerializer.kt index 144b0a0..5906544 100644 --- a/src/main/kotlin/util/json/BlockPosSerializer.kt +++ b/src/main/kotlin/util/json/BlockPosSerializer.kt @@ -5,7 +5,7 @@ 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 +import net.minecraft.core.BlockPos object BlockPosSerializer : KSerializer<BlockPos> { val delegate = serializer<List<Int>>() diff --git a/src/main/kotlin/util/json/CodecSerializer.kt b/src/main/kotlin/util/json/CodecSerializer.kt new file mode 100644 index 0000000..9ea08ad --- /dev/null +++ b/src/main/kotlin/util/json/CodecSerializer.kt @@ -0,0 +1,26 @@ +package util.json + +import com.mojang.serialization.Codec +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonElement +import moe.nea.firmament.util.json.KJsonOps + +abstract class CodecSerializer<T>(val codec: Codec<T>) : KSerializer<T> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeSerializableValue( + JsonElement.serializer(), + codec.encodeStart(KJsonOps.INSTANCE, value).orThrow + ) + } + + override fun deserialize(decoder: Decoder): T { + return codec.decode(KJsonOps.INSTANCE, decoder.decodeSerializableValue(JsonElement.serializer())) + .orThrow.first + } +} diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt index acb1dc8..f4b073a 100644 --- a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt +++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt @@ -9,7 +9,7 @@ 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<UUID> { override val descriptor: SerialDescriptor = @@ -17,10 +17,7 @@ object DashlessUUIDSerializer : KSerializer<UUID> { override fun deserialize(decoder: Decoder): UUID { val str = decoder.decodeString() - if ("-" in str) { - return UUID.fromString(str) - } - return parseDashlessUUID(str) + return parsePotentiallyDashlessUUID(str) } override fun serialize(encoder: Encoder, value: UUID) { diff --git a/src/main/kotlin/util/json/FirmCodecs.kt b/src/main/kotlin/util/json/FirmCodecs.kt index c0863bc..d7b8f57 100644 --- a/src/main/kotlin/util/json/FirmCodecs.kt +++ b/src/main/kotlin/util/json/FirmCodecs.kt @@ -4,11 +4,11 @@ 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 +import net.minecraft.core.UUIDUtil object FirmCodecs { @JvmField - val UUID_LENIENT_PREFER_INT_STREAM = Codec.withAlternative(Uuids.INT_STREAM_CODEC, Codec.STRING.comapFlatMap( + val UUID_LENIENT_PREFER_INT_STREAM = Codec.withAlternative(UUIDUtil.CODEC, Codec.STRING.comapFlatMap( { try { DataResult.success(UndashedUuid.fromStringLenient(it), Lifecycle.stable()) diff --git a/src/main/kotlin/util/json/InstantAsLongSerializer.kt b/src/main/kotlin/util/json/InstantAsLongSerializer.kt index ad738dc..51b5f0a 100644 --- a/src/main/kotlin/util/json/InstantAsLongSerializer.kt +++ b/src/main/kotlin/util/json/InstantAsLongSerializer.kt @@ -2,7 +2,7 @@ package moe.nea.firmament.util.json -import kotlinx.datetime.Instant +import java.time.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -13,10 +13,10 @@ import kotlinx.serialization.encoding.Encoder object InstantAsLongSerializer : KSerializer<Instant> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsLongSerializer", PrimitiveKind.LONG) override fun deserialize(decoder: Decoder): Instant { - return Instant.fromEpochMilliseconds(decoder.decodeLong()) + return Instant.ofEpochMilli(decoder.decodeLong()) } override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeLong(value.toEpochMilliseconds()) + encoder.encodeLong(value.toEpochMilli()) } } 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 <T : JsonElement> List<T>.asJsonArray(): JsonArray { + return JsonArray(this) +} + +fun Iterable<String>.toJsonArray(): JsonArray = map { JsonPrimitive(it) }.asJsonArray() diff --git a/src/main/kotlin/util/json/jsonConversion.kt b/src/main/kotlin/util/json/jsonConversion.kt new file mode 100644 index 0000000..f921f7b --- /dev/null +++ b/src/main/kotlin/util/json/jsonConversion.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.util.json + +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 + + +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/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 <T> Iterable<T>.frequencies(): Map<T, Int> { + val acc = mutableMapOf<T, Int>() + for (t in this) { + acc.compute(t, { _, old -> (old ?: 0) + 1 }) + } + return acc + } + + fun <T> findMostCommonlySharedElement( + leftChain: List<T>, + rightChain: List<T>, + ): 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 <T> List<T>.getMod(index: Int): T { + return this[index.mod(size)] + } + + fun <T> List<T>.rotated(offset: Int): List<T> { + val newList = mutableListOf<T>() + for (index in indices) { + newList.add(getMod(index - offset)) + } + return newList + } + + fun <T> shiftToFront(list: List<T>, element: T): List<T> { + val shiftDistance = list.indexOf(element) + require(shiftDistance >= 0) + return list.rotated(-shiftDistance) + } + + fun <T> List<T>.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE + + fun <T> reconcileCycles( + leftChain: List<T>, + rightChain: List<T>, + ): List<T> { + 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 <T> isValidCycle(longList: List<T>, cycle: List<T>): Boolean { + for ((i, value) in longList.withIndex()) { + if (cycle.getMod(i) != value) + return false + } + return true + } + + fun <T> List<T>.shortenCycle(): List<T> { + for (i in (1..<size)) { + if (isValidCycle(this, subList(0, i))) + return subList(0, i) + } + return this + } + +} diff --git a/src/main/kotlin/util/math/Projections.kt b/src/main/kotlin/util/math/Projections.kt new file mode 100644 index 0000000..9e9f844 --- /dev/null +++ b/src/main/kotlin/util/math/Projections.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.util.math + +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sin +import net.minecraft.world.phys.Vec2 +import moe.nea.firmament.util.render.wrapAngle + +object Projections { + object Two { + val ε = 1e-6 + val π = moe.nea.firmament.util.render.π + val τ = 2 * π + + fun isNullish(float: Float) = float.absoluteValue < ε + + fun xInterceptOfLine(origin: Vec2, direction: Vec2): Vec2? { + if (isNullish(direction.x)) + return Vec2(origin.x, 0F) + if (isNullish(direction.y)) + return null + + val slope = direction.y / direction.x + return Vec2(origin.x - origin.y / slope, 0F) + } + + fun interceptAlongCardinal(distanceFromAxis: Float, slope: Float): Float? { + if (isNullish(slope)) + return null + return -distanceFromAxis / slope + } + + fun projectAngleOntoUnitBox(angleRadians: Double): Vec2 { + val angleRadians = wrapAngle(angleRadians) + val cx = cos(angleRadians) + val cy = sin(angleRadians) + + val ex = 1 / cx.absoluteValue + val ey = 1 / cy.absoluteValue + + val e = minOf(ex, ey) + + return Vec2((cx * e).toFloat(), (cy * e).toFloat()) + } + } +} diff --git a/src/main/kotlin/util/mc/ArmorUtil.kt b/src/main/kotlin/util/mc/ArmorUtil.kt new file mode 100644 index 0000000..3bb1768 --- /dev/null +++ b/src/main/kotlin/util/mc/ArmorUtil.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity + +val LivingEntity.iterableArmorItems + get() = EquipmentSlot.entries.asSequence() + .map { it to getItemBySlot(it) } diff --git a/src/main/kotlin/util/mc/CustomRenderPassHelper.kt b/src/main/kotlin/util/mc/CustomRenderPassHelper.kt new file mode 100644 index 0000000..93cd7c1 --- /dev/null +++ b/src/main/kotlin/util/mc/CustomRenderPassHelper.kt @@ -0,0 +1,161 @@ +package moe.nea.firmament.util.mc + +import com.mojang.blaze3d.buffers.GpuBuffer +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.buffers.Std140Builder +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.systems.RenderPass +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.OptionalDouble +import java.util.OptionalInt +import org.joml.Vector3f +import org.joml.Vector4f +import com.mojang.blaze3d.pipeline.RenderTarget +import com.mojang.blaze3d.vertex.BufferBuilder +import com.mojang.blaze3d.vertex.MeshData +import net.minecraft.client.renderer.texture.AbstractTexture +import com.mojang.blaze3d.vertex.ByteBufferBuilder +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.Mth +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC + + +class CustomRenderPassHelper( + val labelSupplier: () -> String, + val drawMode: VertexFormat.Mode, + val vertexFormat: VertexFormat, + val frameBuffer: RenderTarget, + val hasDepth: Boolean, +) : AutoCloseable { + private val scope = mutableListOf<AutoCloseable>() + private val preparations = mutableListOf<(RenderPass) -> Unit>() + val device = RenderSystem.getDevice() + private var hasPipelineAction = false + private var hasSetDefaultUniforms = false + val commandEncoder = device.createCommandEncoder() + fun setPipeline(pipeline: RenderPipeline) { + ErrorUtil.softCheck("Already has a pipeline", !hasPipelineAction) + hasPipelineAction = true + queueAction { + it.setPipeline(pipeline) + } + } + + fun bindSampler(name: String, texture: ResourceLocation) { + bindSampler(name, MC.textureManager.getTexture(texture)) + } + + fun bindSampler(name: String, texture: AbstractTexture) { + queueAction { it.bindSampler(name, texture.textureView) } + } + + + fun dontSetDefaultUniforms() { + hasSetDefaultUniforms = true + } + + fun setAllDefaultUniforms() { + hasSetDefaultUniforms = true + queueAction { + RenderSystem.bindDefaultUniforms(it) + } + setUniform( + "DynamicTransforms", RenderSystem.getDynamicUniforms() + .writeTransform( + RenderSystem.getModelViewMatrix(), + Vector4f(1.0F, 1.0F, 1.0F, 1.0F), + Vector3f(), // TODO: 1.21.10 + RenderSystem.getTextureMatrix(), + RenderSystem.getShaderLineWidth() + ) + ) + } + + fun setUniform(name: String, slice: GpuBufferSlice) = queueAction { it.setUniform(name, slice) } + fun setUniform(name: String, slice: GpuBuffer) = queueAction { it.setUniform(name, slice) } + + fun setUniform(name: String, size: Int, labelSupplier: () -> String = { name }, init: (Std140Builder) -> Unit) { + val buffer = createUniformBuffer(labelSupplier, allocateByteBuf(size, init)) + setUniform(name, buffer) + } + + var vertices: MeshData? = null + + fun uploadVertices(size: Int, init: (BufferBuilder) -> Unit) { + uploadVertices( + BufferBuilder(queueClose(ByteBufferBuilder(size)), drawMode, vertexFormat) + .also(init) + .buildOrThrow() + ) + } + + fun uploadVertices(buffer: MeshData) { + queueClose(buffer) + ErrorUtil.softCheck("Vertices have already been uploaded", vertices == null) + vertices = buffer + val vertexBuffer = vertexFormat.uploadImmediateVertexBuffer(buffer.vertexBuffer()) + val indexBufferConstructor = RenderSystem.getSequentialBuffer(drawMode) + val indexBuffer = indexBufferConstructor.getBuffer(buffer.drawState().indexCount) + queueAction { + it.setIndexBuffer(indexBuffer, indexBufferConstructor.type()) + it.setVertexBuffer(0, vertexBuffer) + } + } + + fun createUniformBuffer(labelSupplier: () -> String, buffer: ByteBuffer): GpuBuffer { + return queueClose( + device.createBuffer( + labelSupplier::invoke, + GpuBuffer.USAGE_UNIFORM or GpuBuffer.USAGE_MAP_READ, + buffer + ) + ) + } + + fun allocateByteBuf(size: Int, init: (Std140Builder) -> Unit): ByteBuffer { + return Std140Builder.intoBuffer( // TODO: i really dont know about this 16 align? but it seems to be generally correct. + ByteBuffer + .allocateDirect(Mth.roundToward(size, 16)) + .order(ByteOrder.nativeOrder()) + ).also(init).get() + } + + fun queueAction(action: (RenderPass) -> Unit) { + preparations.add(action) + } + + fun <T : AutoCloseable> queueClose(t: T): T = t.also { scope.add(it) } + override fun close() { + scope.reversed().forEach { it.close() } + } + + object DrawToken + + fun draw(): DrawToken { + val vertexData = (ErrorUtil.notNullOr(vertices, "No vertex data uploaded") { return DrawToken }) + ErrorUtil.softCheck("Missing default uniforms", hasSetDefaultUniforms) + ErrorUtil.softCheck("Missing a pipeline", hasPipelineAction) + val renderPass = queueClose( + commandEncoder.createRenderPass( + labelSupplier::invoke, + RenderSystem.outputColorTextureOverride ?: frameBuffer.colorTextureView!!, + OptionalInt.empty(), + (RenderSystem.outputDepthTextureOverride + ?: frameBuffer.depthTextureView).takeIf { frameBuffer.useDepth && hasDepth }, + OptionalDouble.empty() + ) + ) + preparations.forEach { it(renderPass) } + renderPass.drawIndexed( + 0, + 0, + vertexData.drawState().indexCount, + 1 + ) + return DrawToken + } +} diff --git a/src/main/kotlin/util/mc/FakeInventory.kt b/src/main/kotlin/util/mc/FakeInventory.kt index 26c04bc..198ec68 100644 --- a/src/main/kotlin/util/mc/FakeInventory.kt +++ b/src/main/kotlin/util/mc/FakeInventory.kt @@ -1,14 +1,14 @@ package util.mc -import net.minecraft.entity.player.PlayerEntity -import net.minecraft.inventory.Inventory -import net.minecraft.item.ItemStack +import net.minecraft.world.entity.player.Player +import net.minecraft.world.Container +import net.minecraft.world.item.ItemStack -class FakeInventory(val stack: ItemStack) : Inventory { - override fun clear() { +class FakeInventory(val stack: ItemStack) : Container { + override fun clearContent() { } - override fun size(): Int { + override fun getContainerSize(): Int { return 1 } @@ -16,26 +16,26 @@ class FakeInventory(val stack: ItemStack) : Inventory { return stack.isEmpty } - override fun getStack(slot: Int): ItemStack { + override fun getItem(slot: Int): ItemStack { require(slot == 0) return stack } - override fun removeStack(slot: Int, amount: Int): ItemStack { + override fun removeItem(slot: Int, amount: Int): ItemStack { return ItemStack.EMPTY } - override fun removeStack(slot: Int): ItemStack { + override fun removeItemNoUpdate(slot: Int): ItemStack { return ItemStack.EMPTY } - override fun setStack(slot: Int, stack: ItemStack?) { + override fun setItem(slot: Int, stack: ItemStack?) { } - override fun markDirty() { + override fun setChanged() { } - override fun canPlayerUse(player: PlayerEntity?): Boolean { + override fun stillValid(player: Player?): Boolean { return true } } diff --git a/src/main/kotlin/util/mc/FakeSlot.kt b/src/main/kotlin/util/mc/FakeSlot.kt index a9be484..9793fdf 100644 --- a/src/main/kotlin/util/mc/FakeSlot.kt +++ b/src/main/kotlin/util/mc/FakeSlot.kt @@ -1,15 +1,15 @@ package moe.nea.firmament.util.mc import util.mc.FakeInventory -import net.minecraft.item.ItemStack -import net.minecraft.screen.slot.Slot +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.Slot class FakeSlot( - stack: ItemStack, - x: Int, - y: Int + stack: ItemStack, + x: Int, + y: Int ) : Slot(FakeInventory(stack), 0, x, y) { init { - id = 0 + index = 0 } } diff --git a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt index 012f52e..79536e5 100644 --- a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt +++ b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt @@ -1,12 +1,15 @@ package moe.nea.firmament.util.mc import com.mojang.serialization.Codec -import net.minecraft.component.ComponentType -import net.minecraft.registry.Registries -import net.minecraft.registry.Registry +import io.netty.buffer.ByteBuf +import net.minecraft.core.component.DataComponentType +import net.minecraft.network.codec.StreamCodec +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.core.Registry import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ClientInitEvent +import moe.nea.firmament.repo.MiningRepoData object FirmamentDataComponentTypes { @@ -16,20 +19,41 @@ object FirmamentDataComponentTypes { private fun <T> register( id: String, - builderOperator: (ComponentType.Builder<T>) -> Unit - ): ComponentType<T> { + builderOperator: (DataComponentType.Builder<T>) -> Unit + ): DataComponentType<T> { return Registry.register( - Registries.DATA_COMPONENT_TYPE, + BuiltInRegistries.DATA_COMPONENT_TYPE, Firmament.identifier(id), - ComponentType.builder<T>().also(builderOperator) + DataComponentType.builder<T>().also(builderOperator) .build() ) } + fun <T> errorCodec(message: String): StreamCodec<in ByteBuf, T> = + object : StreamCodec<ByteBuf, T> { + override fun decode(buf: ByteBuf?): T? { + error(message) + } + + override fun encode(buf: ByteBuf?, value: T?) { + error(message) + } + } + + fun <T, B : DataComponentType.Builder<T>> B.neverEncode(message: String = "This element should never be encoded or decoded"): B { + networkSynchronized(errorCodec(message)) + persistent(null) + return this + } + val IS_BROKEN = register<Boolean>( "is_broken" ) { - it.codec(Codec.BOOL.fieldOf("is_broken").codec()) + it.persistent(Codec.BOOL.fieldOf("is_broken").codec()) + } + + val CUSTOM_MINING_BLOCK_DATA = register<MiningRepoData.CustomMiningBlock>("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..537ca5b --- /dev/null +++ b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.resources.ResourceLocation + +interface IntrospectableItemModelManager { + fun hasModel_firmament(identifier: ResourceLocation): Boolean +} diff --git a/src/main/kotlin/util/mc/InventoryUtil.kt b/src/main/kotlin/util/mc/InventoryUtil.kt index 74f7b9f..0509138 100644 --- a/src/main/kotlin/util/mc/InventoryUtil.kt +++ b/src/main/kotlin/util/mc/InventoryUtil.kt @@ -2,26 +2,26 @@ package moe.nea.firmament.util.mc import java.util.Spliterator import java.util.Spliterators -import net.minecraft.inventory.Inventory -import net.minecraft.item.ItemStack +import net.minecraft.world.Container +import net.minecraft.world.item.ItemStack -val Inventory.indices get() = 0 until size() -val Inventory.iterableView +val Container.indices get() = 0 until containerSize +val Container.iterableView get() = object : Iterable<ItemStack> { override fun spliterator(): Spliterator<ItemStack> { - return Spliterators.spliterator(iterator(), size().toLong(), 0) + return Spliterators.spliterator(iterator(), containerSize.toLong(), 0) } override fun iterator(): Iterator<ItemStack> { return object : Iterator<ItemStack> { var i = 0 override fun hasNext(): Boolean { - return i < size() + return i < containerSize } override fun next(): ItemStack { if (!hasNext()) throw NoSuchElementException() - return getStack(i++) + return getItem(i++) } } } diff --git a/src/main/kotlin/util/mc/ItemUtil.kt b/src/main/kotlin/util/mc/ItemUtil.kt index 13519cf..91b6409 100644 --- a/src/main/kotlin/util/mc/ItemUtil.kt +++ b/src/main/kotlin/util/mc/ItemUtil.kt @@ -1,20 +1,30 @@ package moe.nea.firmament.util.mc -import net.minecraft.item.ItemStack -import net.minecraft.text.Text +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.NbtOps +import net.minecraft.resources.RegistryOps +import net.minecraft.core.HolderLookup +import net.minecraft.network.chat.Component +import moe.nea.firmament.util.MC -fun ItemStack.appendLore(args: List<Text>) { - if (args.isEmpty()) return - modifyLore { - val loreList = loreAccordingToNbt.toMutableList() - for (arg in args) { - loreList.add(arg) - } - loreList - } +fun ItemStack.appendLore(args: List<Component>) { + if (args.isEmpty()) return + modifyLore { + val loreList = loreAccordingToNbt.toMutableList() + for (arg in args) { + loreList.add(arg) + } + loreList + } } -fun ItemStack.modifyLore(update: (List<Text>) -> List<Text>) { - val loreList = loreAccordingToNbt - loreAccordingToNbt = update(loreList) +fun ItemStack.modifyLore(update: (List<Component>) -> List<Component>) { + val loreList = loreAccordingToNbt + loreAccordingToNbt = update(loreList) +} + +fun loadItemFromNbt(nbt: CompoundTag, registries: HolderLookup.Provider = MC.defaultRegistries): ItemStack? { + return ItemStack.CODEC.decode(RegistryOps.create(NbtOps.INSTANCE, registries), nbt).result().getOrNull()?.first } diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt new file mode 100644 index 0000000..56933d9 --- /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.components.PlayerTabOverlay +import net.minecraft.nbt.NbtOps +import net.minecraft.world.scores.PlayerTeam +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +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 PlayerTabOverlay.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(Component.literal(ComponentSerialization.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<Component>, + val footer: Optional<Component>, + val body: List<Component>, + ) { + companion object { + val CODEC: Codec<CurrentTabList> = RecordCodecBuilder.create { + it.group( + ComponentSerialization.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header), + ComponentSerialization.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer), + ComponentSerialization.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.tabList.cast() + val entries = hud.collectPlayerEntries_firmament() + .map { + it.tabListDisplayName ?: run { + val team = it.team + val name = it.profile.name + PlayerTeam.formatNameForTeam(team, Component.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 index 0c49862..55bfac3 100644 --- a/src/main/kotlin/util/mc/NbtItemData.kt +++ b/src/main/kotlin/util/mc/NbtItemData.kt @@ -1,22 +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 +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ItemLore +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component -var ItemStack.loreAccordingToNbt: List<Text> - get() = get(DataComponentTypes.LORE)?.lines ?: listOf() +var ItemStack.loreAccordingToNbt: List<Component> + get() = get(DataComponents.LORE)?.lines ?: listOf() set(value) { - set(DataComponentTypes.LORE, LoreComponent(value)) + set(DataComponents.LORE, ItemLore(value)) } -var ItemStack.displayNameAccordingToNbt: Text - get() = get(DataComponentTypes.CUSTOM_NAME) ?: get(DataComponentTypes.ITEM_NAME) ?: item.name +var ItemStack.displayNameAccordingToNbt: Component + get() = get(DataComponents.CUSTOM_NAME) ?: get(DataComponents.ITEM_NAME) ?: item.name set(value) { - set(DataComponentTypes.CUSTOM_NAME, value) + set(DataComponents.CUSTOM_NAME, value) } -fun ItemStack.setCustomName(text: Text) { - set(DataComponentTypes.CUSTOM_NAME, text) +fun ItemStack.setCustomName(text: Component) { + set(DataComponents.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..6ac7cb2 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtPrism.kt @@ -0,0 +1,85 @@ +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.serialization.JsonOps +import kotlin.jvm.optionals.getOrNull +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.StringTag +import moe.nea.firmament.util.Base64Util + +class NbtPrism(val path: List<String>) { + 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<NbtPrism> { + override fun parse(reader: StringReader): NbtPrism? { + return fromElement(JsonPrimitive(StringArgumentType.string().parse(reader))) + } + + override fun getExamples(): Collection<String?>? { + return listOf("some.nbt.path", "some.other.*", "some.path.*json.in.a.json.string") + } + } + + override fun toString(): String { + return "Prism($path)" + } + + fun access(root: Tag): Collection<Tag> { + var rootSet = mutableListOf(root) + var switch = mutableListOf<Tag>() + 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(StringTag.valueOf(Base64Util.decodeString(string))) + } + } + } + for (element in rootSet) { + if (element is ListTag) { + if (pathSegment == "*") + switch.addAll(element) + val index = pathSegment.toIntOrNull() ?: continue + if (index !in element.indices) continue + switch.add(element[index]) + } + if (element is CompoundTag) { + if (pathSegment == "*") + element.keySet().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..cfd4184 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtUtil.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.world.item.component.CustomData +import net.minecraft.nbt.Tag +import net.minecraft.nbt.ListTag +import moe.nea.firmament.mixins.accessor.AccessorNbtComponent + +fun Iterable<Tag>.toNbtList() = ListTag().also { + for (element in this) { + it.add(element) + } +} + +@Suppress("CAST_NEVER_SUCCEEDS") +val CustomData.unsafeNbt get() = (this as AccessorNbtComponent).unsafeNbt_firmament diff --git a/src/main/kotlin/util/mc/PlayerUtil.kt b/src/main/kotlin/util/mc/PlayerUtil.kt new file mode 100644 index 0000000..7c21987 --- /dev/null +++ b/src/main/kotlin/util/mc/PlayerUtil.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.player.Player + + +val Player.mainHandStack get() = this.getItemBySlot(EquipmentSlot.MAINHAND) diff --git a/src/main/kotlin/util/mc/Rectangle.kt b/src/main/kotlin/util/mc/Rectangle.kt new file mode 100644 index 0000000..6495c29 --- /dev/null +++ b/src/main/kotlin/util/mc/Rectangle.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.util.mc + +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.navigation.ScreenAxis +import net.minecraft.client.gui.navigation.ScreenRectangle + +fun Rectangle.asScreenRectangle() = + ScreenRectangle.of( + ScreenAxis.HORIZONTAL, + x, y, width, height + ) diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt index e773927..0e630eb 100644 --- a/src/main/kotlin/util/mc/SNbtFormatter.kt +++ b/src/main/kotlin/util/mc/SNbtFormatter.kt @@ -1,22 +1,23 @@ package moe.nea.firmament.util.mc -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 { +import net.minecraft.nbt.CollectionTag +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.ByteArrayTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.DoubleTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.EndTag +import net.minecraft.nbt.FloatTag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.IntArrayTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.LongTag +import net.minecraft.nbt.LongArrayTag +import net.minecraft.nbt.ShortTag +import net.minecraft.nbt.StringTag +import net.minecraft.nbt.TagVisitor + +class SNbtFormatter private constructor() : TagVisitor { private val result = StringBuilder() private var indent = 0 private fun writeIndent() { @@ -31,52 +32,52 @@ class SNbtFormatter private constructor() : NbtElementVisitor { indent-- } - fun apply(element: NbtElement): StringBuilder { + fun apply(element: Tag): StringBuilder { element.accept(this) return result } - override fun visitString(element: NbtString) { - result.append(NbtString.escape(element.asString())) + override fun visitString(element: StringTag) { + result.append(StringTag.quoteAndEscape(element.value)) } - override fun visitByte(element: NbtByte) { - result.append(element.numberValue()).append("b") + override fun visitByte(element: ByteTag) { + result.append(element.box()).append("b") } - override fun visitShort(element: NbtShort) { + override fun visitShort(element: ShortTag) { result.append(element.shortValue()).append("s") } - override fun visitInt(element: NbtInt) { + override fun visitInt(element: IntTag) { result.append(element.intValue()) } - override fun visitLong(element: NbtLong) { + override fun visitLong(element: LongTag) { result.append(element.longValue()).append("L") } - override fun visitFloat(element: NbtFloat) { + override fun visitFloat(element: FloatTag) { result.append(element.floatValue()).append("f") } - override fun visitDouble(element: NbtDouble) { + override fun visitDouble(element: DoubleTag) { result.append(element.doubleValue()).append("d") } - private fun visitArrayContents(array: List<NbtElement>) { + private fun visitArrayContents(array: CollectionTag) { array.forEachIndexed { index, element -> writeIndent() element.accept(this) - if (array.size != index + 1) { + if (array.size() != index + 1) { result.append(",") } result.append("\n") } } - private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) { + private fun writeArray(arrayTypeTag: String, array: CollectionTag) { result.append("[").append(arrayTypeTag).append("\n") pushIndent() visitArrayContents(array) @@ -86,30 +87,30 @@ class SNbtFormatter private constructor() : NbtElementVisitor { } - override fun visitByteArray(element: NbtByteArray) { + override fun visitByteArray(element: ByteArrayTag) { writeArray("B;", element) } - override fun visitIntArray(element: NbtIntArray) { + override fun visitIntArray(element: IntArrayTag) { writeArray("I;", element) } - override fun visitLongArray(element: NbtLongArray) { + override fun visitLongArray(element: LongArrayTag) { writeArray("L;", element) } - override fun visitList(element: NbtList) { + override fun visitList(element: ListTag) { writeArray("", element) } - override fun visitCompound(compound: NbtCompound) { + override fun visitCompound(compound: CompoundTag) { result.append("{\n") pushIndent() - val keys = compound.keys.sorted() + val keys = compound.keySet().sorted() keys.forEachIndexed { index, key -> writeIndent() val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound") - val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key) + val escapedName = escapeName(key) result.append(escapedName).append(": ") element.accept(this) if (keys.size != index + 1) { @@ -122,17 +123,20 @@ class SNbtFormatter private constructor() : NbtElementVisitor { result.append("}") } - override fun visitEnd(element: NbtEnd) { + override fun visitEnd(element: EndTag) { result.append("END") } companion object { - fun prettify(nbt: NbtElement): String { + fun prettify(nbt: Tag): String { return SNbtFormatter().apply(nbt).toString() } - fun NbtElement.toPrettyString() = prettify(this) + fun Tag.toPrettyString() = prettify(this) - private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex() + fun escapeName(key: String): String = + if (key.matches(SIMPLE_NAME)) key else StringTag.quoteAndEscape(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 index 36feb6b..4e3dbf1 100644 --- a/src/main/kotlin/util/mc/ScreenUtil.kt +++ b/src/main/kotlin/util/mc/ScreenUtil.kt @@ -1,9 +1,9 @@ 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 +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.inventory.Slot object ScreenUtil { private var lastScreen: Screen? = null @@ -12,15 +12,15 @@ object ScreenUtil { data class SlotIndex(val index: Int, val isPlayerInventory: Boolean) fun Screen.getSlotsByIndex(): Map<SlotIndex, Slot> { - if (this !is HandledScreen<*>) return mapOf() + if (this !is AbstractContainerScreen<*>) return mapOf() if (lastScreen === this) return slotsByIndex lastScreen = this - slotsByIndex = this.screenHandler.slots.associate { - SlotIndex(it.index, it.inventory is PlayerInventory) to it + slotsByIndex = this.menu.slots.associate { + SlotIndex(it.containerSlot, it.container is Inventory) to it } return slotsByIndex } - fun Screen.getSlotByIndex( index: Int, isPlayerInventory: Boolean): Slot? = + 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 index 0405b65..80028af 100644 --- a/src/main/kotlin/util/mc/SkullItemData.kt +++ b/src/main/kotlin/util/mc/SkullItemData.kt @@ -2,19 +2,20 @@ package moe.nea.firmament.util.mc +import com.google.common.collect.Multimap +import com.google.common.collect.Multimaps import com.mojang.authlib.GameProfile import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.properties.Property +import com.mojang.authlib.properties.PropertyMap +import java.time.Instant import java.util.UUID -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers -import kotlinx.serialization.encodeToString -import net.minecraft.component.DataComponentTypes -import net.minecraft.component.type.ProfileComponent -import net.minecraft.item.ItemStack -import net.minecraft.item.Items +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items import moe.nea.firmament.Firmament import moe.nea.firmament.util.Base64Util.padToValidBase64 import moe.nea.firmament.util.assertTrueOr @@ -23,66 +24,75 @@ import moe.nea.firmament.util.json.InstantAsLongSerializer @Serializable data class MinecraftProfileTextureKt( - val url: String, - val metadata: Map<String, String> = mapOf(), + val url: String, + val metadata: Map<String, String> = mapOf(), ) @Serializable data class MinecraftTexturesPayloadKt( - val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt> = mapOf(), - val profileId: UUID? = null, - val profileName: String? = null, - val isPublic: Boolean = true, - val timestamp: Instant = Clock.System.now(), + val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt> = 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)) +fun createSkullTextures(textures: MinecraftTexturesPayloadKt): PropertyMap { + val json = Firmament.json.encodeToString(textures) + val encoded = java.util.Base64.getEncoder().encodeToString(json.encodeToByteArray()) + return PropertyMap( + Multimaps.forMap(mapOf(propertyTextures to 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)) + assert(this.item == Items.PLAYER_HEAD) + val gameProfile = GameProfile( + uuid, "LameGuy123", + PropertyMap( + Multimaps.forMap( + mapOf(propertyTextures to Property(propertyTextures, encodedData.padToValidBase64())) + ) + ) + ) + this.set(DataComponents.PROFILE, ResolvableProfile.createResolved(gameProfile)) } -val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1") +val arbitraryUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1") fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD) - .also { it.setSkullOwner(uuid, url) } + .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)) + assert(this.item == Items.PLAYER_HEAD) + val gameProfile = GameProfile( + uuid, "nea89", createSkullTextures( + MinecraftTexturesPayloadKt( + textures = mapOf(MinecraftProfileTexture.Type.SKIN to MinecraftProfileTextureKt(url)), + profileId = uuid, + profileName = "nea89", + ) + ) + ) + this.set(DataComponents.PROFILE, ResolvableProfile.createResolved(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<MinecraftTexturesPayloadKt>(json) - } catch (e: Exception) { - // Malformed profile data - if (Firmament.DEBUG) - e.printStackTrace() - null - } + 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<MinecraftTexturesPayloadKt>(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 index 4709dcf..2f5fd49 100644 --- a/src/main/kotlin/util/mc/SlotUtils.kt +++ b/src/main/kotlin/util/mc/SlotUtils.kt @@ -1,34 +1,46 @@ package moe.nea.firmament.util.mc -import net.minecraft.screen.ScreenHandler -import net.minecraft.screen.slot.Slot -import net.minecraft.screen.slot.SlotActionType +import org.lwjgl.glfw.GLFW +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType import moe.nea.firmament.util.MC object SlotUtils { - fun Slot.clickMiddleMouseButton(handler: ScreenHandler) { - MC.interactionManager?.clickSlot( - handler.syncId, - this.id, - 2, - SlotActionType.CLONE, + fun Slot.clickMiddleMouseButton(handler: AbstractContainerMenu) { + MC.interactionManager?.handleInventoryMouseClick( + handler.containerId, + this.index, + GLFW.GLFW_MOUSE_BUTTON_MIDDLE, + ClickType.CLONE, MC.player ) } - fun Slot.swapWithHotBar(handler: ScreenHandler, hotbarIndex: Int) { - MC.interactionManager?.clickSlot( - handler.syncId, this.id, - hotbarIndex, SlotActionType.SWAP, - MC.player) + fun Slot.swapWithHotBar(handler: AbstractContainerMenu, hotbarIndex: Int) { + MC.interactionManager?.handleInventoryMouseClick( + handler.containerId, this.index, + hotbarIndex, ClickType.SWAP, + MC.player + ) + } + + fun Slot.clickRightMouseButton(handler: AbstractContainerMenu) { + MC.interactionManager?.handleInventoryMouseClick( + handler.containerId, + this.index, + GLFW.GLFW_MOUSE_BUTTON_RIGHT, + ClickType.PICKUP, + MC.player + ) } - fun Slot.clickRightMouseButton(handler: ScreenHandler) { - MC.interactionManager?.clickSlot( - handler.syncId, - this.id, - 1, - SlotActionType.PICKUP, + fun Slot.clickLeftMouseButton(handler: AbstractContainerMenu) { + MC.interactionManager?.handleInventoryMouseClick( + handler.containerId, + this.index, + GLFW.GLFW_MOUSE_BUTTON_LEFT, + ClickType.PICKUP, MC.player ) } diff --git a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt index ce596a0..833aca9 100644 --- a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt +++ b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt @@ -2,27 +2,27 @@ 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 +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.RegistryOps +import net.minecraft.core.HolderLookup +import net.minecraft.core.HolderOwner class TolerantRegistriesOps<T>( delegate: DynamicOps<T>, - registryInfoGetter: RegistryInfoGetter + registryInfoGetter: RegistryInfoLookup ) : RegistryOps<T>(delegate, registryInfoGetter) { - constructor(delegate: DynamicOps<T>, registry: RegistryWrapper.WrapperLookup) : - this(delegate, CachedRegistryInfoGetter(registry)) + constructor(delegate: DynamicOps<T>, registry: HolderLookup.Provider) : + this(delegate, HolderLookupAdapter(registry)) - class TolerantOwner<E> : RegistryEntryOwner<E> { - override fun ownerEquals(other: RegistryEntryOwner<E>?): Boolean { + class TolerantOwner<E> : HolderOwner<E> { + override fun canSerializeIn(other: HolderOwner<E>?): Boolean { return true } } - override fun <E : Any?> getOwner(registryRef: RegistryKey<out Registry<out E>>?): Optional<RegistryEntryOwner<E>> { - return super.getOwner(registryRef).map { + override fun <E : Any?> owner(registryRef: ResourceKey<out Registry<out E>>?): Optional<HolderOwner<E>> { + return super.owner(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..1075d62 --- /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.commands.CommandSource +import net.minecraft.commands.CommandSourceStack +import net.minecraft.network.chat.Component + +fun FabricClientCommandSource.asFakeServer(): CommandSourceStack { + val source = this + return CommandSourceStack( + object : CommandSource { + override fun sendSystemMessage(message: Component?) { + source.player.displayClientMessage(message, false) + } + + override fun acceptsSuccess(): Boolean { + return true + } + + override fun acceptsFailure(): Boolean { + return true + } + + override fun shouldInformAdmins(): Boolean { + return true + } + }, + source.position, + source.rotation, + null, + 0, + "FakeServerCommandSource", + Component.literal("FakeServerCommandSource"), + null, + source.player + ) +} diff --git a/src/main/kotlin/util/net/HttpUtil.kt b/src/main/kotlin/util/net/HttpUtil.kt new file mode 100644 index 0000000..1b810e3 --- /dev/null +++ b/src/main/kotlin/util/net/HttpUtil.kt @@ -0,0 +1,86 @@ +package moe.nea.firmament.util.net + +import java.io.InputStream +import java.net.URI +import java.net.URL +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.ByteBuffer +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.Flow +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.serializer +import moe.nea.firmament.Firmament + +object HttpUtil { + val httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build() + + data class Request(val request: HttpRequest.Builder) { + fun <T> execute(bodyHandler: HttpResponse.BodyHandler<T>): CompletableFuture<HttpResponse<T>> { + return httpClient.sendAsync(request.build(), bodyHandler) + } + + fun <T> forBody(bodyHandler: HttpResponse.BodyHandler<T>): CompletableFuture<T> { + return execute(bodyHandler).thenApply { it.body() } + } + + fun forInputStream(): CompletableFuture<InputStream> { + return forBody(HttpResponse.BodyHandlers.ofInputStream()) + } + + inline fun <reified T> forJson(): CompletableFuture<T> { + return forJson(serializer()) + } + + fun <T> forJson(serializer: DeserializationStrategy<T>): CompletableFuture<T> { + return forBody(jsonBodyHandler(serializer)) + } + + fun header(key: String, value: String) { + request.header(key, value) + } + } + + fun <T> jsonBodyHandler(serializer: DeserializationStrategy<T>): HttpResponse.BodyHandler<T> { + val inp = HttpResponse.BodyHandlers.ofInputStream() + return HttpResponse.BodyHandler { + val subscriber = inp.apply(it) + object : HttpResponse.BodySubscriber<T> { + override fun getBody(): CompletionStage<T> { + return subscriber.body.thenApply { Firmament.json.decodeFromStream(serializer, it) } + } + + override fun onSubscribe(subscription: Flow.Subscription?) { + subscriber.onSubscribe(subscription) + } + + override fun onNext(item: List<ByteBuffer?>?) { + subscriber.onNext(item) + } + + override fun onError(throwable: Throwable?) { + subscriber.onError(throwable) + } + + override fun onComplete() { + subscriber.onComplete() + } + } + } + } + + fun request(url: String): Request = request(URI.create(url)) + fun request(url: URL): Request = request(url.toURI()) + fun request(url: URI): Request { + return Request( + HttpRequest.newBuilder(url) + .GET() + .header("user-agent", "Firmament/${Firmament.version}") + ) + } +} diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index a44435c..be6bcfb 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -16,15 +16,23 @@ import kotlin.time.Duration.Companion.seconds inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? = regex.matchEntire(this)?.let(block) -inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? { +inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } - return matcher(string) - .takeIf(Matcher::matches) + return string + ?.let(this::matcher) + ?.takeIf(Matcher::matches) ?.let(block) } +fun <T> 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]" diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt new file mode 100644 index 0000000..4a85c17 --- /dev/null +++ b/src/main/kotlin/util/render/CustomRenderLayers.kt @@ -0,0 +1,105 @@ +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.Mode +import java.util.function.Function +import net.minecraft.client.renderer.RenderPipelines +import com.mojang.blaze3d.shaders.UniformType +import net.minecraft.client.renderer.RenderType +import net.minecraft.client.renderer.RenderStateShard +import com.mojang.blaze3d.vertex.DefaultVertexFormat +import net.minecraft.resources.ResourceLocation +import net.minecraft.Util +import moe.nea.firmament.Firmament + +object CustomRenderPipelines { + val GUI_TEXTURED_NO_DEPTH_TRIS = + RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, Mode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris")) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .build() + val OMNIPRESENT_LINES = RenderPipeline + .builder(RenderPipelines.LINES_SNIPPET) + .withLocation(Firmament.identifier("lines")) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .build() + val COLORED_OMNIPRESENT_QUADS = + RenderPipeline.builder(RenderPipelines.MATRICES_PROJECTION_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(DefaultVertexFormat.POSITION_COLOR, Mode.QUADS) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .withBlend(BlendFunction.TRANSLUCENT) + .build() + + val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS = + RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, Mode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle")) + .withUniform("CutoutRadius", UniformType.UNIFORM_BUFFER) + .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.UNIFORM_BUFFER) + .build() +} + +object CustomRenderLayers { + inline fun memoizeTextured(crossinline func: (ResourceLocation) -> RenderType.CompositeRenderType) = memoize(func) + inline fun <T, R> memoize(crossinline func: (T) -> R): Function<T, R> { + return Util.memoize { it: T -> func(it) } + } + + val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture -> + RenderType.create( + "firmament_gui_textured_overlay_tris", + RenderType.TRANSIENT_BUFFER_SIZE, + CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, + RenderType.CompositeState.builder().setTextureState( + RenderStateShard.TextureStateShard(texture, false) + ) + .createCompositeState(false) + ) + } + val LINES = RenderType.create( + "firmament_lines", + RenderType.TRANSIENT_BUFFER_SIZE, + CustomRenderPipelines.OMNIPRESENT_LINES, + RenderType.CompositeState.builder() // TODO: accept linewidth here + .createCompositeState(false) + ) + val COLORED_QUADS = RenderType.create( + "firmament_quads", + RenderType.TRANSIENT_BUFFER_SIZE, + false, true, + CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS, + RenderType.CompositeState.builder() + .setLightmapState(RenderStateShard.NO_LIGHTMAP) + .createCompositeState(false) + ) + + val TRANSLUCENT_CIRCLE_GUI = + RenderType.create( + "firmament_circle_gui", + RenderType.TRANSIENT_BUFFER_SIZE, + CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS, + RenderType.CompositeState.builder() + .createCompositeState(false) + ) +} diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt index a143d4d..9ef66f3 100644 --- a/src/main/kotlin/util/render/DrawContextExt.kt +++ b/src/main/kotlin/util/render/DrawContextExt.kt @@ -2,63 +2,33 @@ package moe.nea.firmament.util.render import com.mojang.blaze3d.systems.RenderSystem import me.shedaniel.math.Color -import org.joml.Matrix4f -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderLayer.MultiPhaseParameters -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.VertexFormat -import net.minecraft.client.render.VertexFormat.DrawMode -import net.minecraft.client.render.VertexFormats -import net.minecraft.util.Identifier -import net.minecraft.util.TriState -import net.minecraft.util.Util +import org.joml.Vector3f +import util.render.CustomRenderLayers +import kotlin.math.abs +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.renderer.MultiBufferSource +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.MC -fun DrawContext.isUntranslatedGuiDrawContext(): Boolean { - return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0 -} - -object GuiRenderLayers { - val GUI_TEXTURED_NO_DEPTH = Util.memoize<Identifier, RenderLayer> { texture: Identifier -> - RenderLayer.of("firmament_gui_textured_no_depth", - VertexFormats.POSITION_TEXTURE_COLOR, - DrawMode.QUADS, - DEFAULT_BUFFER_SIZE, - MultiPhaseParameters.builder() - .texture(RenderPhase.Texture(texture, TriState.FALSE, false)) - .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .build(false)) - } - val GUI_TEXTURED_TRIS = Util.memoize { texture: Identifier -> - RenderLayer.of("firmament_gui_textured_overlay_tris", - VertexFormats.POSITION_TEXTURE_COLOR, - DrawMode.TRIANGLES, - DEFAULT_BUFFER_SIZE, - MultiPhaseParameters.builder() - .texture(RenderPhase.Texture(texture, TriState.DEFAULT, false)) - .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .writeMaskState(RenderPhase.COLOR_MASK) - .build(false)) - } +fun GuiGraphics.isUntranslatedGuiDrawContext(): Boolean { + return pose().m00 == 1F && pose().m11 == 1f && pose().m01 == 0F && pose().m10 == 0F && pose().m20 == 0F && pose().m21 == 0F } @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 GuiGraphics.drawGuiTexture( + x: Int, y: Int, z: Int, width: Int, height: Int, sprite: ResourceLocation +) = this.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, x, y, width, height) -fun DrawContext.drawGuiTexture( - sprite: Identifier, +fun GuiGraphics.drawGuiTexture( + sprite: ResourceLocation, x: Int, y: Int, width: Int, height: Int -) = this.drawGuiTexture(RenderLayer::getGuiTextured, sprite, x, y, width, height) +) = this.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, x, y, width, height) -fun DrawContext.drawTexture( - sprite: Identifier, +fun GuiGraphics.drawTexture( + sprite: ResourceLocation, x: Int, y: Int, u: Float, @@ -68,34 +38,130 @@ fun DrawContext.drawTexture( textureWidth: Int, textureHeight: Int ) { - this.drawTexture(RenderLayer::getGuiTextured, - sprite, - x, - y, - u, - v, - width, - height, - width, - height, - textureWidth, - textureHeight) + this.blit( + RenderPipelines.GUI_TEXTURED, + sprite, + x, + y, + u, + v, + width, + height, + width, + height, + textureWidth, + textureHeight + ) +} + +data class LineRenderState( + override val x1: Int, + override val x2: Int, + override val y1: Int, + override val y2: Int, + override val scale: Float, + override val bounds: ScreenRectangle, + val lineWidth: Float, + val w: Int, + val h: Int, + val color: Int, + val direction: LineDirection, +) : MultiSpecialGuiRenderState() { + enum class LineDirection { + TOP_LEFT_TO_BOTTOM_RIGHT, + BOTTOM_LEFT_TO_TOP_RIGHT, + } + + override fun createRenderer(vertexConsumers: MultiBufferSource.BufferSource): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState> { + return LineRenderer(vertexConsumers) + } + + override val scissorArea = null } -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 +class LineRenderer(vertexConsumers: MultiBufferSource.BufferSource) : + MultiSpecialGuiRenderer<LineRenderState>(vertexConsumers) { + override fun getRenderStateClass(): Class<LineRenderState> { + return LineRenderState::class.java + } + + override fun getTranslateY(height: Int, windowScaleFactor: Int): Float { + return height / 2F + } + + override fun renderToTexture( + state: LineRenderState, + matrices: PoseStack + ) { + val gr = MC.instance.gameRenderer + val client = MC.instance + gr.globalSettingsUniform + .update( + state.bounds.width, + state.bounds.height, + client.options.glintStrength().get(), + client.level?.gameTime ?: 0L, + client.deltaTracker, + client.options.menuBackgroundBlurriness + ) + + RenderSystem.lineWidth(state.lineWidth) + val buf = bufferSource.getBuffer(CustomRenderLayers.LINES) + val matrix = matrices.last() + val wh = state.w / 2F + val hh = state.h / 2F + val lowX = -wh + val lowY = if (state.direction == LineRenderState.LineDirection.BOTTOM_LEFT_TO_TOP_RIGHT) hh else -hh + val highX = wh + val highY = -lowY + val norm = Vector3f(highX - lowX, highY - lowY, 0F).normalize() + buf.addVertex(matrix, lowX, lowY, 0F).setColor(state.color) + .setNormal(matrix, norm) + buf.addVertex(matrix, highX, highY, 0F).setColor(state.color) + .setNormal(matrix, norm) + bufferSource.endBatch() + gr.globalSettingsUniform + .update( + client.window.width, + client.window.height, + client.options.glintStrength().get(), + client.level?.gameTime ?: 0L, + client.deltaTracker, + client.options.menuBackgroundBlurriness + ) + + } + + override fun getTextureLabel(): String? { + return "Firmament Line Renderer" + } +} + + +fun GuiGraphics.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Color, lineWidth: Float = 1F) { if (toY < fromY) { drawLine(toX, toY, fromX, fromY, color) return } - RenderSystem.lineWidth(MC.window.scaleFactor.toFloat()) - draw { vertexConsumers -> - val buf = vertexConsumers.getBuffer(RenderInWorldContext.RenderLayers.LINES) - buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) - .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) - buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color) - .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) - } + val originalRect = ScreenRectangle( + minOf(fromX, toX), minOf(toY, fromY), + abs(toX - fromX), abs(toY - fromY) + ).transformAxisAligned(pose()) + val expansionFactor = 3 + val rect = ScreenRectangle( + originalRect.left() - expansionFactor, + originalRect.top() - expansionFactor, + originalRect.width + expansionFactor * 2, + originalRect.height + expansionFactor * 2 + ) + // TODO: expand the bounds so that the thickness of the line can be used + // TODO: fix this up to work with scissorarea + guiRenderState.submitPicturesInPictureState( + LineRenderState( + rect.left(), rect.right(), rect.top(), rect.bottom(), 1F, rect, lineWidth, + originalRect.width, originalRect.height, color.color, + if (fromX < toX) LineRenderState.LineDirection.TOP_LEFT_TO_BOTTOM_RIGHT else LineRenderState.LineDirection.BOTTOM_LEFT_TO_TOP_RIGHT + ) + ) } diff --git a/src/main/kotlin/util/render/DumpTexture.kt b/src/main/kotlin/util/render/DumpTexture.kt new file mode 100644 index 0000000..2ac6a1c --- /dev/null +++ b/src/main/kotlin/util/render/DumpTexture.kt @@ -0,0 +1,34 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.buffers.GpuBuffer +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.textures.GpuTexture +import java.io.File +import com.mojang.blaze3d.platform.NativeImage + +fun dumpTexture(gpuTexture: GpuTexture, name: String) { + val w = gpuTexture.getWidth(0) + val h = gpuTexture.getHeight(0) + val buffer = RenderSystem.getDevice() + .createBuffer( + { "Dump Buffer" }, + GpuBuffer.USAGE_COPY_DST or GpuBuffer.USAGE_MAP_READ, + w * h * gpuTexture.getFormat().pixelSize() + ) + val commandEncoder = RenderSystem.getDevice().createCommandEncoder() + commandEncoder.copyTextureToBuffer( + gpuTexture, buffer, 0, { + val nativeImage = NativeImage(w, h, false) + commandEncoder.mapBuffer(buffer, true, false).use { mappedView -> + for (i in 0..<w) { + for (j in 0..<h) { + val color = mappedView.data().getInt((j + i * w) * gpuTexture.format.pixelSize()) + nativeImage.setPixelABGR(j, h - i - 1, color) + } + } + } + buffer.close() + nativeImage.writeToFile(File("$name.png")) + }, 0 + ) +} diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt index daa8da9..dc45939 100644 --- a/src/main/kotlin/util/render/FacingThePlayerContext.kt +++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt @@ -1,21 +1,15 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.systems.RenderSystem -import io.github.notenoughupdates.moulconfig.platform.next import org.joml.Matrix4f -import net.minecraft.client.font.TextRenderer -import net.minecraft.client.render.BufferRenderer -import net.minecraft.client.render.GameRenderer -import net.minecraft.client.render.LightmapTextureManager -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.Tessellator -import net.minecraft.client.render.VertexConsumer -import net.minecraft.client.render.VertexFormat -import net.minecraft.client.render.VertexFormats -import net.minecraft.text.Text -import net.minecraft.util.Identifier -import net.minecraft.util.math.BlockPos +import util.render.CustomRenderLayers +import net.minecraft.client.gui.Font +import net.minecraft.client.renderer.LightTexture +import net.minecraft.client.renderer.RenderType +import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.core.BlockPos import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.MC import moe.nea.firmament.util.assertTrueOr @@ -23,76 +17,76 @@ import moe.nea.firmament.util.assertTrueOr @RenderContextDSL class FacingThePlayerContext(val worldContext: RenderInWorldContext) { val matrixStack by worldContext::matrixStack - fun waypoint(position: BlockPos, label: Text) { + fun waypoint(position: BlockPos, label: Component) { text( label, - Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}") + Component.literal("§e${FirmFormatters.formatDistance(MC.player?.position?.distanceTo(position.center) ?: 42069.0)}") ) } fun text( - vararg texts: Text, - verticalAlign: RenderInWorldContext.VerticalAlign = RenderInWorldContext.VerticalAlign.CENTER, - background: Int = 0x70808080, + vararg texts: Component, + 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.pushPose() + val width = MC.font.width(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.vertexConsumers.getBuffer(RenderType.textBackgroundSeeThrough()) + val matrix4f = worldContext.matrixStack.last().pose() + vertexConsumer.addVertex(matrix4f, -1.0f, -1.0f, 0.0f).setColor(background) + .setLight(LightTexture.FULL_BLOCK) + vertexConsumer.addVertex(matrix4f, -1.0f, MC.font.lineHeight.toFloat(), 0.0f).setColor(background) + .setLight(LightTexture.FULL_BLOCK) + vertexConsumer.addVertex(matrix4f, width.toFloat(), MC.font.lineHeight.toFloat(), 0.0f) + .setColor(background) + .setLight(LightTexture.FULL_BLOCK) + vertexConsumer.addVertex(matrix4f, width.toFloat(), -1.0f, 0.0f).setColor(background) + .setLight(LightTexture.FULL_BLOCK) worldContext.matrixStack.translate(0F, 0F, 0.01F) - MC.font.draw( + MC.font.drawInBatch( text, 0F, 0F, -1, false, - worldContext.matrixStack.peek().positionMatrix, + worldContext.matrixStack.last().pose(), worldContext.vertexConsumers, - TextRenderer.TextLayerType.SEE_THROUGH, + Font.DisplayMode.SEE_THROUGH, 0, - LightmapTextureManager.MAX_LIGHT_COORDINATE + LightTexture.FULL_BRIGHT ) - worldContext.matrixStack.pop() + worldContext.matrixStack.popPose() } } fun texture( - texture: Identifier, width: Int, height: Int, - u1: Float, v1: Float, - u2: Float, v2: Float, + texture: ResourceLocation, width: Int, height: Int, + u1: Float, v1: Float, + u2: Float, v2: Float, ) { - val buf = worldContext.vertexConsumers.getBuffer(RenderLayer.getGuiTexturedOverlay(texture)) + val buf = worldContext.vertexConsumers.getBuffer(CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture)) // TODO: this is strictly an incorrect render layer 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() + val matrix4f: Matrix4f = worldContext.matrixStack.last().pose() + buf.addVertex(matrix4f, -hw, -hh, 0F) + .setColor(-1) + .setUv(u1, v1) + buf.addVertex(matrix4f, -hw, +hh, 0F) + .setColor(-1) + .setUv(u1, v2) + buf.addVertex(matrix4f, +hw, +hh, 0F) + .setColor(-1) + .setUv(u2, v2) + buf.addVertex(matrix4f, +hw, -hh, 0F) + .setColor(-1) + .setUv(u2, v1) + worldContext.vertexConsumers.endBatch() } } diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt index ba67dbb..53afdf5 100644 --- a/src/main/kotlin/util/render/FirmamentShaders.kt +++ b/src/main/kotlin/util/render/FirmamentShaders.kt @@ -1,30 +1,12 @@ package moe.nea.firmament.util.render -import net.minecraft.client.gl.Defines -import net.minecraft.client.gl.ShaderProgramKey -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.VertexFormat -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 { - val shaders = mutableListOf<ShaderProgramKey>() - - private fun shader(name: String, format: VertexFormat, defines: Defines): ShaderProgramKey { - val key = ShaderProgramKey(Firmament.identifier(name), format, defines) - shaders.add(key) - return key - } - - val LINES = RenderPhase.ShaderProgram(shader("core/rendertype_lines", VertexFormats.LINES, Defines.EMPTY)) @Subscribe fun debugLoad(event: DebugInstantiateEvent) { - shaders.forEach { - MC.instance.shaderLoader.getOrCreateProgram(it) - } + // 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 index f2c2f25..e7f226c 100644 --- a/src/main/kotlin/util/render/LerpUtils.kt +++ b/src/main/kotlin/util/render/LerpUtils.kt @@ -1,33 +1,40 @@ - package moe.nea.firmament.util.render import me.shedaniel.math.Color +import kotlin.math.absoluteValue -val pi = Math.PI -val tau = Math.PI * 2 +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 - val shortestAngle = ((((b.mod(tau) - a.mod(tau)).mod(tau)) + tau + pi).mod(tau)) - pi - return ((a + (shortestAngle) * progress).mod(tau)).toFloat() + // 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 + return a + (b - a) * progress } + fun lerp(a: Int, b: Int, progress: Float): Int { - return (a + (b - a) * progress).toInt() + return (a + (b - a) * progress).toInt() } fun ilerp(a: Float, b: Float, value: Float): Float { - return (value - a) / (b - a) + 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), - ) + 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/MultiSpecialGuiRenderState.kt b/src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt new file mode 100644 index 0000000..a58ffdc --- /dev/null +++ b/src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt @@ -0,0 +1,47 @@ +package moe.nea.firmament.util.render + +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer +import net.minecraft.client.gui.render.state.GuiRenderState +import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState +import net.minecraft.client.renderer.MultiBufferSource + +abstract class MultiSpecialGuiRenderState : PictureInPictureRenderState { + // I wish i had manifolds @Self type here... Maybe i should switch to java after all :( + abstract fun createRenderer(vertexConsumers: MultiBufferSource.BufferSource): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState> + abstract val x1: Int + abstract val x2: Int + abstract val y1: Int + abstract val y2: Int + abstract val scale: Float + abstract val bounds: ScreenRectangle? + abstract val scissorArea: ScreenRectangle? + override fun x0(): Int = x1 + + override fun x1(): Int = x2 + + override fun y0(): Int = y1 + + override fun y1(): Int = y2 + + override fun scale(): Float = scale + + override fun scissorArea(): ScreenRectangle? = scissorArea + + override fun bounds(): ScreenRectangle? = bounds + +} + +abstract class MultiSpecialGuiRenderer<T : MultiSpecialGuiRenderState>( + vertexConsumers: MultiBufferSource.BufferSource +) : PictureInPictureRenderer<T>(vertexConsumers) { + var wasUsedThisFrame = false + fun consumeRender(): Boolean { + return wasUsedThisFrame.also { wasUsedThisFrame = false } + } + + override fun blitTexture(element: T, state: GuiRenderState) { + wasUsedThisFrame = true + super.blitTexture(element, state) + } +} diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt index 805633c..acd0210 100644 --- a/src/main/kotlin/util/render/RenderCircleProgress.kt +++ b/src/main/kotlin/util/render/RenderCircleProgress.kt @@ -1,93 +1,166 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.systems.RenderSystem -import io.github.notenoughupdates.moulconfig.platform.next -import org.joml.Matrix4f -import org.joml.Vector2f -import kotlin.math.atan2 -import kotlin.math.tan -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.BufferRenderer -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.Tessellator -import net.minecraft.client.render.VertexFormat.DrawMode -import net.minecraft.client.render.VertexFormats -import net.minecraft.util.Identifier +import com.mojang.blaze3d.vertex.VertexFormat +import util.render.CustomRenderLayers +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.navigation.ScreenRectangle +import com.mojang.blaze3d.vertex.BufferBuilder +import net.minecraft.client.renderer.RenderType +import net.minecraft.client.renderer.MultiBufferSource +import com.mojang.blaze3d.vertex.ByteBufferBuilder +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith +import moe.nea.firmament.util.math.Projections +import moe.nea.firmament.util.mc.CustomRenderPassHelper object RenderCircleProgress { - fun renderCircle( - drawContext: DrawContext, - texture: Identifier, - progress: Float, - u1: Float, - u2: Float, - v1: Float, - v2: Float, - ) { - RenderSystem.enableBlend() - drawContext.draw { - val bufferBuilder = it.getBuffer(GuiRenderLayers.GUI_TEXTURED_TRIS.apply(texture)) - val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix - - val corners = listOf( - Vector2f(0F, -1F), - Vector2f(1F, -1F), - Vector2f(1F, 0F), - Vector2f(1F, 1F), - Vector2f(0F, 1F), - Vector2f(-1F, 1F), - Vector2f(-1F, 0F), - Vector2f(-1F, -1F), - ) - for (i in (0 until 8)) { - if (progress < i / 8F) { - break + data class State( + override val x1: Int, + override val x2: Int, + override val y1: Int, + override val y2: Int, + val layer: RenderType.CompositeRenderType, + val u1: Float, + val u2: Float, + val v1: Float, + val v2: Float, + val angleRadians: ClosedFloatingPointRange<Float>, + val color: Int, + val innerCutoutRadius: Float, + override val scale: Float, + override val bounds: ScreenRectangle?, + override val scissorArea: ScreenRectangle?, + ) : MultiSpecialGuiRenderState() { + override fun createRenderer(vertexConsumers: MultiBufferSource.BufferSource): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState> { + return Renderer(vertexConsumers) + } + } + + class Renderer(vertexConsumers: MultiBufferSource.BufferSource) : + MultiSpecialGuiRenderer<State>(vertexConsumers) { + override fun renderToTexture( + state: State, + matrices: PoseStack + ) { + matrices.pushPose() + matrices.translate(0F, -1F, 0F) + val sections = state.angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat()) + .zipWithNext().toList() + val u1 = state.u1 + val u2 = state.u2 + val v1 = state.v1 + val v2 = state.v2 + val color = state.color + val matrix = matrices.last().pose() + ByteBufferBuilder(state.layer.format().vertexSize * sections.size * 3).use { allocator -> + + val bufferBuilder = BufferBuilder(allocator, VertexFormat.Mode.TRIANGLES, state.layer.format()) + + 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 + .addVertex(matrix, secondPoint.x, secondPoint.y, 0F) + .setUv(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y))) + .setColor(color) + + bufferBuilder + .addVertex(matrix, firstPoint.x, firstPoint.y, 0F) + .setUv(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y))) + .setColor(color) + + bufferBuilder + .addVertex(matrix, 0F, 0F, 0F) + .setUv(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) + .setColor(color) + } - val second = corners[(i + 1) % 8] - val first = corners[i] - if (progress <= (i + 1) / 8F) { - val internalProgress = 1 - (progress - i / 8F) * 8F - val angle = lerpAngle( - atan2(second.y, second.x), - atan2(first.y, first.x), - internalProgress - ) - if (angle < tau / 8 || angle >= tau * 7 / 8) { - second.set(1F, tan(angle)) - } else if (angle < tau * 3 / 8) { - second.set(1 / tan(angle), 1F) - } else if (angle < tau * 5 / 8) { - second.set(-1F, -tan(angle)) - } else { - second.set(-1 / tan(angle), -1F) + + bufferBuilder.buildOrThrow().use { buffer -> + if (state.innerCutoutRadius <= 0) { + state.layer.draw(buffer) + return + } + CustomRenderPassHelper( + { "RenderCircleProgress" }, + VertexFormat.Mode.TRIANGLES, + state.layer.format(), + MC.instance.mainRenderTarget, + false, + ).use { renderPass -> + renderPass.uploadVertices(buffer) + renderPass.setAllDefaultUniforms() + renderPass.setPipeline(state.layer.renderPipeline) + renderPass.setUniform("CutoutRadius", 4) { + it.putFloat(state.innerCutoutRadius) + } + renderPass.draw() } } - - fun ilerp(f: Float): Float = - ilerp(-1f, 1f, f) - - bufferBuilder - .vertex(matrix, second.x, second.y, 0F) - .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y))) - .color(-1) - .next() - bufferBuilder - .vertex(matrix, first.x, first.y, 0F) - .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y))) - .color(-1) - .next() - bufferBuilder - .vertex(matrix, 0F, 0F, 0F) - .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) - .color(-1) - .next() } + matrices.popPose() + } + + override fun getRenderStateClass(): Class<State> { + return State::class.java + } + + override fun getTextureLabel(): String { + return "Firmament Circle" } - RenderSystem.disableBlend() } + fun renderCircularSlice( + drawContext: GuiGraphics, + layer: RenderType.CompositeRenderType, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + angleRadians: ClosedFloatingPointRange<Float>, + color: Int = -1, + innerCutoutRadius: Float = 0F + ) { + val screenRect = ScreenRectangle(-1, -1, 2, 2).transformAxisAligned(drawContext.pose()) + drawContext.guiRenderState.submitPicturesInPictureState( + State( + screenRect.left(), screenRect.right(), + screenRect.top(), screenRect.bottom(), + layer, + u1, u2, v1, v2, + angleRadians, + color, + innerCutoutRadius, + screenRect.width / 2F, + screenRect, + null + ) + ) + } + fun renderCircle( + drawContext: GuiGraphics, + texture: ResourceLocation, + progress: Float, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + color: Int = -1 + ) { + renderCircularSlice( + drawContext, + CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture), + u1, u2, v1, v2, + (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat(), + color = color + ) + } } diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt index bb58200..f6877c8 100644 --- a/src/main/kotlin/util/render/RenderInWorldContext.kt +++ b/src/main/kotlin/util/render/RenderInWorldContext.kt @@ -1,89 +1,80 @@ 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 net.minecraft.client.gl.VertexBuffer -import net.minecraft.client.render.Camera -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.RenderPhase -import net.minecraft.client.render.RenderTickCounter -import net.minecraft.client.render.Tessellator -import net.minecraft.client.render.VertexConsumer -import net.minecraft.client.render.VertexConsumerProvider -import net.minecraft.client.render.VertexFormat -import net.minecraft.client.render.VertexFormats -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 util.render.CustomRenderLayers +import kotlin.math.pow +import net.minecraft.client.Camera +import net.minecraft.client.renderer.RenderType +import net.minecraft.client.renderer.ItemBlockRenderTypes +import net.minecraft.client.DeltaTracker +import net.minecraft.client.renderer.Sheets +import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.client.renderer.state.CameraRenderState +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.MC @RenderContextDSL class RenderInWorldContext private constructor( - private val tesselator: Tessellator, - val matrixStack: MatrixStack, - private val camera: Camera, - private val tickCounter: RenderTickCounter, - val vertexConsumers: VertexConsumerProvider.Immediate, + val matrixStack: PoseStack, + private val camera: CameraRenderState, + val vertexConsumers: MultiBufferSource.BufferSource, ) { - - object RenderLayers { - val TRANSLUCENT_TRIS = RenderLayer.of("firmament_translucent_tris", - VertexFormats.POSITION_COLOR, - VertexFormat.DrawMode.TRIANGLES, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, true, - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .program(RenderPhase.POSITION_COLOR_PROGRAM) - .build(false)) - val LINES = RenderLayer.of("firmament_rendertype_lines", - VertexFormats.LINES, - VertexFormat.DrawMode.LINES, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, false, // do we need translucent? i dont think so - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .program(FirmamentShaders.LINES) - .build(false) - ) - val COLORED_QUADS = RenderLayer.of( - "firmament_quads", - VertexFormats.POSITION_COLOR, - VertexFormat.DrawMode.QUADS, - RenderLayer.CUTOUT_BUFFER_SIZE, - false, true, - RenderLayer.MultiPhaseParameters.builder() - .depthTest(RenderPhase.ALWAYS_DEPTH_TEST) - .program(RenderPhase.POSITION_COLOR_PROGRAM) - .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) - .build(false) - ) - } - - @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) + fun block(blockPos: BlockPos, color: Int) { + matrixStack.pushPose() + matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat()) + buildCube(matrixStack.last().pose(), vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) + matrixStack.popPose() } - @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 box(aabb: AABB, color: Int) { + matrixStack.pushPose() + matrixStack.translate(aabb.minX, aabb.minY, aabb.minZ) + matrixStack.scale(aabb.xsize.toFloat(), aabb.ysize.toFloat(), aabb.zsize.toFloat()) + buildCube(matrixStack.last().pose(), vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) + matrixStack.popPose() } - 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(RenderLayers.COLORED_QUADS), color) - matrixStack.pop() + fun sharedVoxelSurface(blocks: Set<BlockPos>, color: Int) { + val m = BlockPos.MutableBlockPos() + val l = vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS) + blocks.forEach { + matrixStack.pushPose() + matrixStack.translate(it.x.toFloat(), it.y.toFloat(), it.z.toFloat()) + val p = matrixStack.last().pose() + 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.popPose() + } } enum class VerticalAlign { @@ -91,46 +82,46 @@ class RenderInWorldContext private constructor( 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()) + CENTER -> (index - count / 2F) * (1 + MC.font.lineHeight.toFloat()) + BOTTOM -> (index - count) * (1 + MC.font.lineHeight.toFloat()) + TOP -> (index) * (1 + MC.font.lineHeight.toFloat()) } } } - fun waypoint(position: BlockPos, vararg label: Text) { + fun waypoint(position: BlockPos, vararg label: Component) { text( - position.toCenterPos(), + position.center, *label, - Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}"), + Component.literal("§e${FirmFormatters.formatDistance(MC.player?.position?.distanceTo(position.center) ?: 42069.0)}"), background = 0xAA202020.toInt() ) } - fun withFacingThePlayer(position: Vec3d, block: FacingThePlayerContext.() -> Unit) { - matrixStack.push() + fun withFacingThePlayer(position: Vec3, block: FacingThePlayerContext.() -> Unit) { + matrixStack.pushPose() 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) + val vec = position.subtract(camera.pos).scale(distanceToMoveTowardsCamera / actualCameraDistance) matrixStack.translate(vec.x, vec.y, vec.z) - matrixStack.multiply(camera.rotation) + matrixStack.mulPose(camera.orientation) matrixStack.scale(0.025F, -0.025F, 1F) FacingThePlayerContext(this).run(block) - matrixStack.pop() - vertexConsumers.drawCurrentLayer() + matrixStack.popPose() + vertexConsumers.endLastBatch() } - fun sprite(position: Vec3d, sprite: Sprite, width: Int, height: Int) { + fun sprite(position: Vec3, sprite: TextureAtlasSprite, width: Int, height: Int) { texture( - position, sprite.atlasId, width, height, sprite.minU, sprite.minV, sprite.maxU, sprite.maxV + position, sprite.atlasLocation(), width, height, sprite.u0, sprite.v0, sprite.u1, sprite.v1 ) } fun texture( - position: Vec3d, texture: Identifier, width: Int, height: Int, + position: Vec3, texture: ResourceLocation, width: Int, height: Int, u1: Float, v1: Float, u2: Float, v2: Float, ) { @@ -140,8 +131,8 @@ class RenderInWorldContext private constructor( } fun text( - position: Vec3d, - vararg texts: Text, + position: Vec3, + vararg texts: Component, verticalAlign: VerticalAlign = VerticalAlign.CENTER, background: Int = 0x70808080 ) { @@ -150,42 +141,50 @@ class RenderInWorldContext private constructor( } } - fun tinyBlock(vec3d: Vec3d, size: Float, color: Int) { - matrixStack.push() + fun tinyBlock(vec3d: Vec3, size: Float, color: Int) { + matrixStack.pushPose() matrixStack.translate(vec3d.x, vec3d.y, vec3d.z) matrixStack.scale(size, size, size) matrixStack.translate(-.5, -.5, -.5) - buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color) - matrixStack.pop() - vertexConsumers.draw() + buildCube(matrixStack.last().pose(), vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) + matrixStack.popPose() + vertexConsumers.endBatch() } fun wireframeCube(blockPos: BlockPos, lineWidth: Float = 10F) { - val buf = vertexConsumers.getBuffer(RenderLayer.LINES) - matrixStack.push() + val buf = vertexConsumers.getBuffer(RenderType.LINES) + matrixStack.pushPose() + // TODO: add color arg to this // 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() + RenderSystem.lineWidth(lineWidth / camera.pos.distanceToSqr(blockPos.center).pow(0.25).toFloat()) + val offset = 1 / 512F + matrixStack.translate( + blockPos.x.toFloat() - offset, + blockPos.y.toFloat() - offset, + blockPos.z.toFloat() - offset + ) + val scale = 1 + 2 * offset + matrixStack.scale(scale, scale, scale) + + buildWireFrameCube(matrixStack.last(), buf) + matrixStack.popPose() + vertexConsumers.endBatch() } - fun line(vararg points: Vec3d, lineWidth: Float = 10F) { - line(points.toList(), lineWidth) + fun line(vararg points: Vec3, color: Int, lineWidth: Float = 10F) { + line(points.toList(), color, 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 tracer(toWhere: Vec3, color: Int, lineWidth: Float = 3f) { + val cameraForward = Vector3f(0f, 0f, -1f).rotate(camera.orientation) + line(camera.pos.add(Vec3(cameraForward)), toWhere, color = color, lineWidth = lineWidth) } - fun line(points: List<Vec3d>, lineWidth: Float = 10F) { + fun line(points: List<Vec3>, color: Int, lineWidth: Float = 10F) { RenderSystem.lineWidth(lineWidth) - // TODO: replace with renderlayers - val buffer = tesselator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES) + val buffer = vertexConsumers.getBuffer(CustomRenderLayers.LINES) - val matrix = matrixStack.peek() + val matrix = matrixStack.last() var lastNormal: Vector3f? = null points.zipWithNext().forEach { (a, b) -> val normal = Vector3f(b.x.toFloat(), b.y.toFloat(), b.z.toFloat()) @@ -193,23 +192,22 @@ class RenderInWorldContext private constructor( .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() + buffer.addVertex(matrix.pose(), a.x.toFloat(), a.y.toFloat(), a.z.toFloat()) + .setColor(color) + .setNormal(matrix, lastNormal0.x, lastNormal0.y, lastNormal0.z) + + buffer.addVertex(matrix.pose(), b.x.toFloat(), b.y.toFloat(), b.z.toFloat()) + .setColor(color) + .setNormal(matrix, normal.x, normal.y, normal.z) + } - RenderLayers.LINES.draw(buffer.end()) } // TODO: put the favourite icons in front of items again companion object { private fun doLine( - matrix: MatrixStack.Entry, + matrix: PoseStack.Pose, buf: VertexConsumer, i: Float, j: Float, @@ -221,18 +219,18 @@ class RenderInWorldContext private constructor( 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() + buf.addVertex(matrix.pose(), i, j, k) + .setNormal(matrix, normal.x, normal.y, normal.z) + .setColor(-1) + + buf.addVertex(matrix.pose(), x, y, z) + .setNormal(matrix, normal.x, normal.y, normal.z) + .setColor(-1) + } - private fun buildWireFrameCube(matrix: MatrixStack.Entry, buf: VertexConsumer) { + private fun buildWireFrameCube(matrix: PoseStack.Pose, buf: VertexConsumer) { for (i in 0..1) { for (j in 0..1) { val i = i.toFloat() @@ -244,68 +242,72 @@ class RenderInWorldContext private constructor( } } - private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, color: Int) { - // Y- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 0F).color(color) - // Y+ - buf.vertex(matrix, 0F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) - // X- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 0F).color(color) - // X+ - buf.vertex(matrix, 1F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) - // Z- - buf.vertex(matrix, 0F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 0F, 0F).color(color) - buf.vertex(matrix, 1F, 1F, 0F).color(color) - buf.vertex(matrix, 0F, 1F, 0F).color(color) - // Z+ - buf.vertex(matrix, 0F, 0F, 1F).color(color) - buf.vertex(matrix, 0F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 1F, 1F).color(color) - buf.vertex(matrix, 1F, 0F, 1F).color(color) + private fun buildFaceZP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 0F, 0F, 1F).setColor(rgba) + buf.addVertex(matrix, 0F, 1F, 1F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 1F).setColor(rgba) + buf.addVertex(matrix, 1F, 0F, 1F).setColor(rgba) } + private fun buildFaceZN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 0F, 0F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 0F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 0F).setColor(rgba) + buf.addVertex(matrix, 0F, 1F, 0F).setColor(rgba) + } + + private fun buildFaceXP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 1F, 0F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 1F).setColor(rgba) + buf.addVertex(matrix, 1F, 0F, 1F).setColor(rgba) + } + + private fun buildFaceXN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 0F, 0F, 0F).setColor(rgba) + buf.addVertex(matrix, 0F, 0F, 1F).setColor(rgba) + buf.addVertex(matrix, 0F, 1F, 1F).setColor(rgba) + buf.addVertex(matrix, 0F, 1F, 0F).setColor(rgba) + } + + private fun buildFaceYN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 0F, 0F, 0F).setColor(rgba) + buf.addVertex(matrix, 0F, 0F, 1F).setColor(rgba) + buf.addVertex(matrix, 1F, 0F, 1F).setColor(rgba) + buf.addVertex(matrix, 1F, 0F, 0F).setColor(rgba) + } + + private fun buildFaceYP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.addVertex(matrix, 0F, 1F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 0F).setColor(rgba) + buf.addVertex(matrix, 1F, 1F, 1F).setColor(rgba) + buf.addVertex(matrix, 0F, 1F, 1F).setColor(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.pushPose() event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z) val ctx = RenderInWorldContext( - RenderSystem.renderThreadTesselator(), event.matrices, event.camera, - event.tickCounter, event.vertexConsumers ) block(ctx) - event.matrices.pop() - event.vertexConsumers.draw() - RenderSystem.setShaderColor(1F, 1F, 1F, 1F) - VertexBuffer.unbind() - RenderSystem.enableDepthTest() - RenderSystem.enableCull() - RenderSystem.disableBlend() + event.matrices.popPose() + event.vertexConsumers.endBatch() } } } diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt new file mode 100644 index 0000000..6513574 --- /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.renderer.texture.OverlayTexture +import net.minecraft.util.ARGB +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.pixels, "Disposed TintedOverlayTexture written to") { return this } + if (color == lastColor) return this + lastColor = color + + for (i in 0..<size) { + for (j in 0..<size) { + if (i < 8) { + image.setPixel(j, i, 0xB2FF0000.toInt()) + } else { + val k = ((1F - j / 15F * 0.75F) * 255F).toInt() + image.setPixel(j, i, ARGB.color(k, color.color)) + } + } + } + + texture.setFilter(false, false) + texture.setClamp(true) + texture.upload() + return this + } +} diff --git a/src/main/kotlin/util/render/TranslatedScissors.kt b/src/main/kotlin/util/render/TranslatedScissors.kt index c1e6544..e337cf0 100644 --- a/src/main/kotlin/util/render/TranslatedScissors.kt +++ b/src/main/kotlin/util/render/TranslatedScissors.kt @@ -1,22 +1,31 @@ - package moe.nea.firmament.util.render -import org.joml.Vector4f -import net.minecraft.client.gui.DrawContext +import me.shedaniel.math.Rectangle +import org.joml.Matrix3x2f +import org.joml.Vector3f +import net.minecraft.client.gui.GuiGraphics + +fun GuiGraphics.enableScissorWithTranslation(rect: Rectangle) { + enableScissor(rect.minX, rect.minY, rect.maxX, rect.maxY) +} + +fun GuiGraphics.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) { + enableScissor(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt()) +} -fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) { - val pMat = matrices.peek().positionMatrix - val target = Vector4f() +fun GuiGraphics.enableScissorWithoutTranslation(x1: Float, y1: Float, x2: Float, y2: Float) { + val pMat = Matrix3x2f(pose()).invert() + var target = Vector3f() - target.set(x1, y1, 0f, 1f) - target.mul(pMat) - val scissorX1 = target.x - val scissorY1 = target.y + target.set(x1, y1, 1F) + target.mul(pMat) + val scissorX1 = target.x + val scissorY1 = target.y - target.set(x2, y2, 0f, 1f) - target.mul(pMat) - val scissorX2 = target.x - val scissorY2 = target.y + target.set(x2, y2, 1F) + target.mul(pMat) + val scissorX2 = target.x + val scissorY2 = target.y - enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt()) + enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt()) } diff --git a/src/main/kotlin/util/skyblock/AbilityUtils.kt b/src/main/kotlin/util/skyblock/AbilityUtils.kt index 0d7d2b7..9ba182d 100644 --- a/src/main/kotlin/util/skyblock/AbilityUtils.kt +++ b/src/main/kotlin/util/skyblock/AbilityUtils.kt @@ -1,8 +1,8 @@ package moe.nea.firmament.util.skyblock import kotlin.time.Duration -import net.minecraft.item.ItemStack -import net.minecraft.text.Text +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.directLiteralStringContent import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -17,7 +17,7 @@ object AbilityUtils { val hasPowerScroll: Boolean, val activation: AbilityActivation, val manaCost: Int?, - val descriptionLines: List<Text>, + val descriptionLines: List<Component>, val cooldown: Duration?, ) @@ -40,7 +40,7 @@ object AbilityUtils { } private val abilityNameRegex = "Ability: (?<name>.*?) *".toPattern() - private fun findAbility(iterator: ListIterator<Text>): ItemAbility? { + private fun findAbility(iterator: ListIterator<Component>): ItemAbility? { if (!iterator.hasNext()) { return null } @@ -72,7 +72,7 @@ object AbilityUtils { return null } if (abilityName == null) return null - val descriptionLines = mutableListOf<Text>() + val descriptionLines = mutableListOf<Component>() var manaCost: Int? = null var cooldown: Duration? = null while (iterator.hasNext()) { @@ -121,7 +121,7 @@ object AbilityUtils { ) } - fun getAbilities(lore: List<Text>): List<ItemAbility> { + fun getAbilities(lore: List<Component>): List<ItemAbility> { val iterator = lore.listIterator() val abilities = mutableListOf<ItemAbility>() while (iterator.hasNext()) { diff --git a/src/main/kotlin/util/skyblock/DungeonUtil.kt b/src/main/kotlin/util/skyblock/DungeonUtil.kt new file mode 100644 index 0000000..c4eb169 --- /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\\s*)+".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: §a1m 46s' +'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 index 6c7096c..887edef 100644 --- a/src/main/kotlin/util/skyblock/ItemType.kt +++ b/src/main/kotlin/util/skyblock/ItemType.kt @@ -1,13 +1,12 @@ package moe.nea.firmament.util.skyblock -import net.minecraft.item.ItemStack +import net.minecraft.world.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) { +data class ItemType private constructor(val name: String) { companion object { fun ofName(name: String): ItemType { return ItemType(name) @@ -41,6 +40,7 @@ value class ItemType private constructor(val name: String) { val SWORD = ofName("SWORD") val DRILL = ofName("DRILL") val PICKAXE = ofName("PICKAXE") + val AXE = ofName("AXE") val GAUNTLET = ofName("GAUNTLET") val LONGSWORD = ofName("LONG SWORD") val EQUIPMENT = ofName("EQUIPMENT") @@ -57,6 +57,8 @@ value class ItemType private constructor(val name: String) { val LEGGINGS = ofName("LEGGINGS") val HELMET = ofName("HELMET") val BOOTS = ofName("BOOTS") + val SHOVEL = ofName("SHOVEL") + val NIL = ofName("__NIL") /** @@ -67,6 +69,8 @@ value class ItemType private constructor(val name: String) { 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/PartyUtil.kt b/src/main/kotlin/util/skyblock/PartyUtil.kt new file mode 100644 index 0000000..46e1aa3 --- /dev/null +++ b/src/main/kotlin/util/skyblock/PartyUtil.kt @@ -0,0 +1,210 @@ +package moe.nea.firmament.util.skyblock + +import java.util.UUID +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket.PartyRole +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket +import org.intellij.lang.annotations.Language +import kotlinx.coroutines.launch +import net.minecraft.network.chat.Component +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.apis.Routes +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.boolColour +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.useMatch + +object PartyUtil { + object Internal { + val hma = HypixelModAPI.getInstance() + + val handler = hma.createHandler(ClientboundPartyInfoPacket::class.java) { clientboundPartyInfoPacket -> + Firmament.coroutineScope.launch { + party = Party(clientboundPartyInfoPacket.memberMap.values.map { + PartyMember.fromUuid(it.uuid, it.role) + }) + } + } + + fun sendSyncPacket() { + hma.sendPacket(ServerboundPartyInfoPacket()) + } + + @Subscribe + fun onDevCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("party") { + thenLiteral("refresh") { + thenExecute { + sendSyncPacket() + source.sendFeedback(tr("firmament.dev.partyinfo.refresh", "Refreshing party info")) + } + } + thenExecute { + val p = party + val text = Component.empty() + text.append( + tr("firmament.dev.partyinfo", "Party Info: ") + .boolColour(p != null) + ) + if (p == null) { + text.append(tr("firmament.dev.partyinfo.empty", "Empty Party").grey()) + } else { + text.append(tr("firmament.dev.partyinfo.count", "${p.members.size} members").grey()) + p.members.forEach { + text.append("\n") + .append(Component.literal(" - ${it.name}")) + .append(" (") + .append( + when (it.role) { + PartyRole.LEADER -> tr("firmament.dev.partyinfo.leader", "Leader").bold() + PartyRole.MOD -> tr("firmament.dev.partyinfo.mod", "Moderator") + PartyRole.MEMBER -> tr("firmament.dev.partyinfo.member", "Member") + } + ) + .append(")") + } + } + source.sendFeedback(text) + } + } + } + } + + object Regexes { + @Language("RegExp") + val NAME = "(\\[[^\\]]+\\] )?(?<name>[a-zA-Z0-9_]{2,16})" + val NAME_SECONDARY = NAME.replace("name", "name2") + val joinSelf = "You have joined $NAME's? party!".toPattern() + val joinOther = "$NAME joined the party\\.".toPattern() + val leaveSelf = "You left the party\\.".toPattern() + val disbandedEmpty = + "The party was disbanded because all invites expired and the party was empty\\.".toPattern() + val leaveOther = "$NAME has left the party\\.".toPattern() + val kickedOther = "$NAME has been removed from the party\\.".toPattern() + val kickedOtherOffline = "Kicked $NAME because they were offline\\.".toPattern() + val disconnectedOther = "$NAME was removed from your party because they disconnected\\.".toPattern() + val transferLeave = "The party was transferred to $NAME because $NAME_SECONDARY left\\.?".toPattern() + val transferVoluntary = "The party was transferred to $NAME by $NAME_SECONDARY\\.?".toPattern() + val disbanded = "$NAME has disbanded the party!".toPattern() + val kickedSelf = "You have been kicked from the party by $NAME ?\\.?".toPattern() + val partyFinderJoin = "Party Finder > $NAME joined the .* group!.*".toPattern() + } + + fun modifyParty( + allowEmpty: Boolean = false, + modifier: (MutableList<PartyMember>) -> Unit + ) { + val oldList = party?.members ?: emptyList() + if (oldList.isEmpty() && !allowEmpty) return + party = Party(oldList.toMutableList().also(modifier)) + } + + fun MutableList<PartyMember>.modifyMember(name: String, mod: (PartyMember) -> PartyMember) { + val idx = indexOfFirst { it.name == name } + val member = if (idx < 0) { + PartyMember(name, PartyRole.MEMBER) + } else { + removeAt(idx) + } + add(mod(member)) + } + + fun addMemberToParty(name: String) { + modifyParty(true) { + if (it.isEmpty()) + it.add(PartyMember(MC.playerName, PartyRole.LEADER)) + it.add(PartyMember(name, PartyRole.MEMBER)) + } + } + + @Subscribe + fun onJoinServer(event: WorldReadyEvent) { // This event isn't perfect... Hypixel isn't ready yet when we join the server. We should probably just listen to the mod api hello packet and go from there, but this works (since you join and leave servers quite often). + if (party == null) + sendSyncPacket() + } + + @Subscribe + fun onPartyRelatedMessage(event: ProcessChatEvent) { + Regexes.joinSelf.useMatch(event.unformattedString) { + sendSyncPacket() + } + Regexes.joinOther.useMatch(event.unformattedString) { + addMemberToParty(group("name")) + } + Regexes.leaveOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.leaveSelf.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.disbandedEmpty.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.kickedOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.kickedOtherOffline.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.disconnectedOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.transferLeave.useMatch(event.unformattedString) { + modifyParty { + it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) } + it.removeIf { it.name == group("name2") } + } + } + Regexes.transferVoluntary.useMatch(event.unformattedString) { + modifyParty { + it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) } + it.modifyMember(group("name2")) { it.copy(role = PartyRole.MOD) } + } + } + Regexes.disbanded.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.kickedSelf.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.partyFinderJoin.useMatch(event.unformattedString) { + addMemberToParty(group("name")) + } + } + } + + data class Party( + val members: List<PartyMember> + ) + + data class PartyMember( + val name: String, + val role: PartyRole + ) { + companion object { + suspend fun fromUuid(uuid: UUID, role: PartyRole = PartyRole.MEMBER): PartyMember { + return PartyMember( + ErrorUtil.notNullOr( + Routes.getPlayerNameForUUID(uuid), + "Could not find username for player $uuid" + ) { "Ghost" }, + role + ) + } + } + } + + var party: Party? = null +} diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt index b19f371..95e5d87 100644 --- a/src/main/kotlin/util/skyblock/Rarity.kt +++ b/src/main/kotlin/util/skyblock/Rarity.kt @@ -7,10 +7,10 @@ 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 net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting import moe.nea.firmament.util.StringUtil.words import moe.nea.firmament.util.collections.lastNotNullOfOrNull import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -31,6 +31,7 @@ enum class Rarity(vararg altNames: String) { SUPREME, SPECIAL, VERY_SPECIAL, + ULTIMATE, UNKNOWN ; @@ -48,22 +49,23 @@ enum class Rarity(vararg altNames: String) { } val names = setOf(name) + altNames - val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this])) + val text: Component get() = Component.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.COMMON to ChatFormatting.WHITE, + Rarity.UNCOMMON to ChatFormatting.GREEN, + Rarity.RARE to ChatFormatting.BLUE, + Rarity.EPIC to ChatFormatting.DARK_PURPLE, + Rarity.LEGENDARY to ChatFormatting.GOLD, + Rarity.MYTHIC to ChatFormatting.LIGHT_PURPLE, + Rarity.DIVINE to ChatFormatting.AQUA, + Rarity.SPECIAL to ChatFormatting.RED, + Rarity.VERY_SPECIAL to ChatFormatting.RED, + Rarity.SUPREME to ChatFormatting.DARK_RED, + Rarity.ULTIMATE to ChatFormatting.DARK_RED, ) val byName = entries.flatMap { en -> en.names.map { it to en } }.toMap() val fromNeuRepo = entries.associateBy { it.neuRepoRarity } @@ -87,7 +89,7 @@ enum class Rarity(vararg altNames: String) { fun fromPetItem(itemStack: ItemStack): Rarity? = itemStack.petData?.tier?.let(::fromNeuRepo) - fun fromLore(lore: List<Text>): Rarity? = + fun fromLore(lore: List<Component>): 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 index 3901b60..619a10b 100644 --- a/src/main/kotlin/util/skyblock/SBItemUtil.kt +++ b/src/main/kotlin/util/skyblock/SBItemUtil.kt @@ -1,12 +1,12 @@ package moe.nea.firmament.util.skyblock -import net.minecraft.item.ItemStack +import net.minecraft.world.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 + val name = this.hoverName.unformattedString if (name.contains("Enchanted Book")) { val enchant = loreAccordingToNbt.firstOrNull()?.unformattedString if (enchant != null) return enchant diff --git a/src/main/kotlin/util/skyblock/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt index fd67c44..a69309b 100644 --- a/src/main/kotlin/util/skyblock/SackUtil.kt +++ b/src/main/kotlin/util/skyblock/SackUtil.kt @@ -2,15 +2,18 @@ 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 net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.network.chat.HoverEvent +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ChestInventoryUpdateEvent import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.gui.config.storage.ConfigFixEvent +import moe.nea.firmament.gui.config.storage.ConfigStorageClass 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.Config import moe.nea.firmament.util.data.ProfileSpecificDataHolder import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.iterableView @@ -28,18 +31,26 @@ object SackUtil { // val sackTypes: ) - object Store : ProfileSpecificDataHolder<SackContents>(serializer(), "Sacks", ::SackContents) + @Config + object Store : ProfileSpecificDataHolder<SackContents>(serializer(), "sacks", ::SackContents) + + @Subscribe + fun onConfigFix(event: ConfigFixEvent) { + event.on(996, ConfigStorageClass.PROFILE) { + move("Sacks", "sacks") + } + } val items get() = Store.data?.contents ?: mutableMapOf() val storedRegex = "^Stored: (?<stored>$SHORT_NUMBER_FORMAT)/(?<max>$SHORT_NUMBER_FORMAT)$".toPattern() @Subscribe fun storeDataFromInventory(event: ChestInventoryUpdateEvent) { - val screen = event.inventory as? GenericContainerScreen ?: return + val screen = event.inventory as? ContainerScreen ?: 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) + val inv = screen.menu?.container ?: return + if (inv.containerSize < 18) return + val backSlot = inv.getItem(inv.containerSize - 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) { @@ -63,7 +74,7 @@ object SackUtil { getUpdatesFromMessage(event.text) } - fun getUpdatesFromMessage(text: Text): List<SackUpdate> { + fun getUpdatesFromMessage(text: Component): List<SackUpdate> { val update = ChatUpdate() text.siblings.forEach(update::updateFromHoverText) return update.updates @@ -91,9 +102,9 @@ object SackUtil { } } - fun updateFromHoverText(text: Text) { + fun updateFromHoverText(text: Component) { text.siblings.forEach(::updateFromHoverText) - val hoverText = text.style.hoverEvent?.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val hoverText = (text.style.hoverEvent as? HoverEvent.ShowText)?.value ?: return val cleanedText = hoverText.unformattedString if (cleanedText.startsWith("Added items:\n")) { if (!foundAdded) { diff --git a/src/main/kotlin/util/skyblock/ScreenIdentification.kt b/src/main/kotlin/util/skyblock/ScreenIdentification.kt new file mode 100644 index 0000000..ff725fa --- /dev/null +++ b/src/main/kotlin/util/skyblock/ScreenIdentification.kt @@ -0,0 +1,52 @@ +package moe.nea.firmament.util.skyblock + +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.unformattedString + + +object ScreenIdentification { + private var lastScreen: Screen? = null + private var lastScreenType: ScreenType? = null + + fun getType(screen: Screen?): ScreenType? { + if (screen == null) return null + if (screen !== lastScreen) { + lastScreenType = ScreenType.entries + .find { it.detector(screen) } + lastScreen = screen + } + return lastScreenType + } +} + +enum class ScreenType(val detector: (Screen) -> Boolean) { + BAZAAR_ANY({ + it is ContainerScreen && ( + it.menu.getSlot(it.menu.rowCount * 9 - 4) + .item + .displayNameAccordingToNbt + .unformattedString == "Manage Orders" + || it.menu.getSlot(it.menu.rowCount * 9 - 5) + .item + .loreAccordingToNbt + .any { + it.unformattedString == "To Bazaar" + }) + }), + ENCHANTMENT_GUIDE({ + it.title.unformattedString.endsWith("Enchantments Guide") + }), + SUPER_PAIRS({ + it.title.unformattedString.startsWith("Superpairs") + }), + EXPERIMENTATION_RNG_METER({ + it.title.unformattedString.contains("Experimentation Table RNG") + }), + DYE_COMPENDIUM({ + it.title.unformattedString.contains("Dye Compendium") + }) +} + diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index cfd8429..5f4acd4 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -3,9 +3,25 @@ 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 ARCHAIC_SPADE = SkyblockId("ARCHAIC_SPADE") + val DEIFIC_SPADE = SkyblockId("DEIFIC_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") + val ETHERWARP_CONDUIT = SkyblockId("ETHERWARP_CONDUIT") } diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt new file mode 100644 index 0000000..43722e0 --- /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.network.chat.Component +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<Component> { + 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: Component): 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 index ab3de43..d8b2592 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -1,71 +1,18 @@ package moe.nea.firmament.util -import net.minecraft.text.ClickEvent -import net.minecraft.text.MutableText -import net.minecraft.text.PlainTextContent -import net.minecraft.text.Text -import net.minecraft.text.TextColor -import net.minecraft.text.TranslatableTextContent -import net.minecraft.util.Formatting -import moe.nea.firmament.Firmament - - -class TextMatcher(text: Text) { - data class State( - var iterator: MutableList<Text>, - var currentText: Text?, - var offset: Int, - var textContent: String, - ) - - var state = State( - mutableListOf(text), - null, - 0, - "" - ) - - fun pollChunk(): Boolean { - val firstOrNull = state.iterator.removeFirstOrNull() ?: return false - state.offset = 0 - state.currentText = firstOrNull - state.textContent = when (val content = firstOrNull.content) { - is PlainTextContent.Literal -> content.string - else -> { - Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.") - return false - } - } - state.iterator.addAll(0, firstOrNull.siblings) - return true - } - - fun pollChunks(): Boolean { - while (state.offset !in state.textContent.indices) { - if (!pollChunk()) { - return false - } - } - return true - } +import java.util.Optional +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.HoverEvent +import net.minecraft.network.chat.MutableComponent +import net.minecraft.util.FormattedCharSequence +import net.minecraft.network.chat.contents.PlainTextContents +import net.minecraft.network.chat.FormattedText +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.TextColor +import net.minecraft.network.chat.contents.TranslatableContents +import net.minecraft.ChatFormatting - fun pollChar(): Char? { - if (!pollChunks()) return null - return state.textContent[state.offset++] - } - - - fun expectString(string: String): Boolean { - var found = "" - while (found.length < string.length) { - if (!pollChunks()) return false - val takeable = state.textContent.drop(state.offset).take(string.length - found.length) - state.offset += takeable.length - found += takeable - } - return found == string - } -} val formattingChars = "kmolnrKMOLNR".toSet() fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { @@ -89,77 +36,159 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { return stringBuffer.toString() } -val Text.unformattedString: String +fun FormattedCharSequence.reconstitute(): MutableComponent { + val base = Component.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(Component.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(codePoint.toChar()) + true + } + if (text.isNotEmpty()) + base.append(Component.literal(text.toString()).setStyle(lastColorCode)) + return base + +} + +fun FormattedText.reconstitute(): MutableComponent { + val base = Component.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(Component.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(string) + Optional.empty<Unit>() + }, Style.EMPTY) + if (text.isNotEmpty()) + base.append(Component.literal(text.toString()).setStyle(lastColorCode)) + return base + +} + +val Component.unformattedString: String get() = string.removeColorCodes() // TODO: maybe shortcircuit this with .visit -val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string() +val Component.directLiteralStringContent: String? get() = (this.contents as? PlainTextContents)?.text() -fun Text.getLegacyFormatString() = +fun Component.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()) { - sb.append(component.style.color?.toChatFormatting()?.toString() ?: "§r") + 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) - sb.append("§r") + 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 } } +private val textColorLUT = ChatFormatting.entries + .mapNotNull { formatting -> formatting.color?.let { it to formatting } } .toMap() -fun TextColor.toChatFormatting(): Formatting? { - return textColorLUT[this.rgb] +fun TextColor.toChatFormatting(): ChatFormatting? { + return textColorLUT[this.value] } -fun Text.iterator(): Sequence<Text> { +fun Component.iterator(): Sequence<Component> { return sequenceOf(this) + siblings.asSequence() .flatMap { it.iterator() } // TODO: in theory we want to properly inherit styles here } -fun Text.allSiblings(): List<Text> = listOf(this) + siblings.flatMap { it.allSiblings() } +fun Component.allSiblings(): List<Component> = listOf(this) + siblings.flatMap { it.allSiblings() } -fun MutableText.withColor(formatting: Formatting): MutableText = this.styled { +fun MutableComponent.withColor(formatting: ChatFormatting): MutableComponent = this.withStyle { 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.red() = withColor(Formatting.RED) -fun MutableText.white() = withColor(Formatting.WHITE) -fun MutableText.bold(): MutableText = styled { it.withBold(true) } - - -fun MutableText.clickCommand(command: String): MutableText { +fun MutableComponent.blue() = withColor(ChatFormatting.BLUE) +fun MutableComponent.aqua() = withColor(ChatFormatting.AQUA) +fun MutableComponent.lime() = withColor(ChatFormatting.GREEN) +fun MutableComponent.darkGreen() = withColor(ChatFormatting.DARK_GREEN) +fun MutableComponent.purple() = withColor(ChatFormatting.DARK_PURPLE) +fun MutableComponent.pink() = withColor(ChatFormatting.LIGHT_PURPLE) +fun MutableComponent.yellow() = withColor(ChatFormatting.YELLOW) +fun MutableComponent.gold() = withColor(ChatFormatting.GOLD) +fun MutableComponent.grey() = withColor(ChatFormatting.GRAY) +fun MutableComponent.darkGrey() = withColor(ChatFormatting.DARK_GRAY) +fun MutableComponent.red() = withColor(ChatFormatting.RED) +fun MutableComponent.white() = withColor(ChatFormatting.WHITE) +fun MutableComponent.bold(): MutableComponent = withStyle { it.withBold(true) } +fun MutableComponent.hover(text: Component): MutableComponent = withStyle { it.withHoverEvent(HoverEvent.ShowText(text)) } +fun MutableComponent.boolColour( + bool: Boolean, + ifTrue: ChatFormatting = ChatFormatting.GREEN, + ifFalse: ChatFormatting = ChatFormatting.DARK_RED +) = + if (bool) withColor(ifTrue) else withColor(ifFalse) + +fun MutableComponent.clickCommand(command: String): MutableComponent { require(command.startsWith("/")) - return this.styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command)) + return this.withStyle { + it.withClickEvent(ClickEvent.RunCommand(command)) } } -fun MutableText.prepend(text: Text): MutableText { +fun MutableComponent.prepend(text: Component): MutableComponent { 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) +fun Component.transformEachRecursively(function: (Component) -> Component): Component { + val c = this.contents + if (c is TranslatableContents) { + return Component.translatableWithFallback(c.key, c.fallback, *c.args.map { + (it as? Component ?: Component.literal(it.toString())).transformEachRecursively(function) }.toTypedArray()).also { new -> new.style = this.style new.siblings.clear() + val new = function(new) this.siblings.forEach { child -> new.siblings.add(child.transformEachRecursively(function)) } @@ -172,6 +201,16 @@ fun Text.transformEachRecursively(function: (Text) -> Text): Text { } } -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 tr(key: String, default: String): MutableComponent = error("Compiler plugin did not run.") +fun trResolved(key: String, vararg args: Any): MutableComponent = Component.translatableEscape(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 index cccfdd2..14aa83d 100644 --- a/src/main/kotlin/util/uuid.kt +++ b/src/main/kotlin/util/uuid.kt @@ -3,6 +3,12 @@ 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) diff --git a/src/main/resources/assets/firmament/gui/button_editor_fragment.xml b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml index 6444236..6656634 100644 --- a/src/main/resources/assets/firmament/gui/button_editor_fragment.xml +++ b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml @@ -1,24 +1,29 @@ <?xml version="1.0" encoding="UTF-8" ?> <Root xmlns="http://notenoughupdates.org/moulconfig"> - <Panel background="VANILLA" insets="10"> - <Column> - <Row> - <ItemStack value="@getItemIcon"/> - <Text text="Icon"/> - </Row> - <Hover lines="Put your display item in here.;Can be any SkyBlock item id.;;Alternatively you can paste in a /give command"> - <TextField value="@icon" width="180"/> - </Hover> - <Text text="Command"/> - <Hover lines="Put the command in here.;The text box should not start with a /"> - <Row> - <Text text="/"/> - <TextField value="@command" width="180"/> - </Row> - </Hover> - <Button onClick="@delete"> - <Text text="Delete"/> - </Button> - </Column> - </Panel> + <Panel background="VANILLA" insets="10"> + <Column> + <Row> + <ItemStack value="@getItemIcon"/> + <Text text="Icon"/> + </Row> + <Hover + lines="Put your display item in here.;Can be any SkyBlock item id.;;Alternatively you can paste in a /give command"> + <TextField value="@icon" width="180"/> + </Hover> + <Row> + <Switch value="@isGigantic"/> + <Text text="Big Button"/> + </Row> + <Text text="Command"/> + <Hover lines="Put the command in here.;The text box should not start with a /"> + <Row> + <Text text="/"/> + <TextField value="@command" width="180"/> + </Row> + </Hover> + <Button onClick="@delete"> + <Text text="Delete"/> + </Button> + </Column> + </Panel> </Root> 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..91edae3 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Panel background="TRANSPARENT" insets="10"> + <Column> + <ScrollPanel width="380" height="300"> + <Align horizontal="CENTER"> + <Array data="@actions"> + <!-- evenBackground="#8B8B8B" oddBackground="#C6C6C6" --> + <Panel background="TRANSPARENT" insets="3"> + <Panel background="VANILLA" insets="6"> + <Column> + <Row> + <Text text="@commandR" width="280"/> + </Row> + <Row> + <Text text="@formattedCombo" width="250"/> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@edit"> + <Text text="Edit"/> + </firm:Button> + <Spacer width="12"/> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Row> + </Align> + </Row> + </Column> + </Panel> + + </Panel> + </Array> + </Align> + </ScrollPanel> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@discard"> + <Text text="Discard Changes"/> + </firm:Button> + <firm:Button onClick="@saveAndClose"> + <Text text="Save & Close"/> + </firm:Button> + <firm:Button onClick="@save"> + <Text text="Save"/> + </firm:Button> + <firm:Button onClick="@addCommand"> + <Text text="Add Combo Command"/> + </firm:Button> + </Row> + </Align> + </Column> + </Panel> +</Root> 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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Center> + <Panel background="VANILLA" insets="10"> + <Column> + <Row> + <firm:Button onClick="@back"> + <Text text="←"/> + </firm:Button> + <Text text="Editing command macro"/> + </Row> + <Row> + <Text text="Command: /"/> + <Align horizontal="RIGHT"> + <TextField value="@command" width="200"/> + </Align> + </Row> + <Row> + <Text text="Key Combo:"/> + <Align horizontal="RIGHT"> + <firm:Button onClick="@addStep"> + <Text text="+"/> + </firm:Button> + </Align> + </Row> + <Array data="@combo"> + <Row> + <firm:Fixed width="160"> + <Indirect value="@button"/> + </firm:Fixed> + <Align horizontal="RIGHT"> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Align> + </Row> + </Array> + </Column> + </Panel> + </Center> +</Root> 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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Center> + <Panel background="VANILLA" insets="10"> + <Column> + <Row> + <firm:Button onClick="@back"> + <Text text="←"/> + </firm:Button> + <Text text="Editing wheel macro"/> + </Row> + <Row> + <Text text="Key (Hold):"/> + <Align horizontal="RIGHT"> + <firm:Fixed width="160"> + <Indirect value="@button"/> + </firm:Fixed> + </Align> + </Row> + <Row> + <Text text="Menu Options:"/> + <Align horizontal="RIGHT"> + <firm:Button onClick="@addOption"> + <Text text="+"/> + </firm:Button> + </Align> + </Row> + <Array data="@editableCommands"> + <Row> + <Text text="/"/> + <TextField value="@text" width="160"/> + <Align horizontal="RIGHT"> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Align> + </Row> + </Array> + </Column> + </Panel> + </Center> +</Root> 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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" +> + <Center> + <Row> + <Tabs> + <Tab> + <Tab.Header> + <Text text="Combo Macros"/> + </Tab.Header> + <Tab.Body> + <Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/> + </Tab.Body> + </Tab> + <Tab> + <Tab.Header> + <Text text="Macro Wheel"/> + </Tab.Header> + <Tab.Body> + <Fragment value="firmament:gui/config/macros/wheel.xml" bind="@wheels"/> + </Tab.Body> + </Tab> + </Tabs> + <Meta beforeClose="@beforeClose"/> + </Row> + </Center> +</Root> 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..80826c9 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Panel background="TRANSPARENT" insets="10"> + <Column> + <ScrollPanel width="380" height="300"> + <Align horizontal="CENTER"> + <Array data="@wheels"> + <Panel background="TRANSPARENT" insets="3"> + <Panel background="VANILLA" insets="6"> + <Column> + <Row> + <Text text="@keyCombo" width="250"/> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@edit"> + <Text text="Edit"/> + </firm:Button> + <Spacer width="12"/> + <firm:Button onClick="@delete"> + <Text text="Delete"/> + </firm:Button> + </Row> + </Align> + </Row> + <Array data="@commands"> + <Text text="@textR" width="280"/> + </Array> + </Column> + </Panel> + + </Panel> + </Array> + </Align> + </ScrollPanel> + <Align horizontal="RIGHT"> + <Row> + <firm:Button onClick="@discard"> + <Text text="Discard Changes"/> + </firm:Button> + <firm:Button onClick="@saveAndClose"> + <Text text="Save & Close"/> + </firm:Button> + <firm:Button onClick="@save"> + <Text text="Save"/> + </firm:Button> + <firm:Button onClick="@addWheel"> + <Text text="Add Wheel"/> + </firm:Button> + </Row> + </Align> + </Column> + </Panel> +</Root> 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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" + xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Center> + <Panel background="VANILLA"> + <Column> + <Center> + <Scale scale="2"> + <Text text="Firmament Licenses"/> + </Scale> + </Center> + <!-- <firm:Line/>--> + <ScrollPanel width="306" height="250"> + <Panel insets="3" background="TRANSPARENT"> + <Array data="@softwares"> + <Center> + <firm:Fixed width="300"> + <Panel background="VANILLA" insets="8"> + <Column> + <Scale scale="1.2"> + <Text text="@projectName"/> + </Scale> + <When condition="@hasWebPresence"> + <Row> + <firm:Button onClick="@open"> + <Text text="Navigate to WebSite"/> + </firm:Button> + </Row> + <Spacer/> + </When> + <Text text="@projectDescription" width="280"/> + <Array data="@developers"> + <Row> + <Text text="by "/> + <Text text="@name"/> + </Row> + </Array> + <Array data="@licenses"> + <When condition="@hasUrl"> + <firm:Button onClick="@open"> + <Center> + <Row> + <Text text="License: "/> + <Text text="@name"/> + </Row> + </Center> + </firm:Button> + <Row> + <Text text="License: "/> + <Text text="@name"/> + </Row> + </When> + </Array> + </Column> + </Panel> + </firm:Fixed> + </Center> + </Array> + </Panel> + </ScrollPanel> + </Column> + </Panel> + </Center> +</Root> 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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<Root xmlns="http://notenoughupdates.org/moulconfig" + xmlns:firm="http://firmament.nea.moe/moulconfig" +> + <Gui> + <Column> + <Row> + <Text text="Search: "/> + <TextField value="@search"/> + </Row> + <ScrollPanel width="200" height="150"> + <Array data="@ores"> + <Column> + <Text text="@oreName"/> + <Array data="@blocks"> + <Row> + <When condition="@isSelected"> + <Center> + <Text text="§a+" textAlign="CENTER" width="10"/> + </Center> + <Spacer width="10" height="0"/> + </When> + <firm:Hover lines="@restrictions"> + <Row> + <ItemStack value="@item"/> + <Align horizontal="LEFT" vertical="CENTER"> + <Text text="@itemName"/> + </Align> + </Row> + </firm:Hover> + </Row> + </Array> + </Column> + </Array> + </ScrollPanel> + </Column> + </Gui> +</Root> diff --git a/src/main/resources/assets/firmament/logo.png b/src/main/resources/assets/firmament/logo.png Binary files differindex e00a2fa..e3f063a 100644 --- a/src/main/resources/assets/firmament/logo.png +++ b/src/main/resources/assets/firmament/logo.png 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..5ee269d --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh @@ -0,0 +1,54 @@ +#version 150 + +#moj_import <minecraft:fog.glsl> +#moj_import <minecraft:dynamictransforms.glsl> + +#define M_PI 3.1415926535897932384626433832795 +#define M_TAU (2.0 * M_PI) +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; +uniform sampler2D Sampler3; + +layout(std140) uniform Animation { + float AnimationPosition; +}; + +in float sphericalVertexDistance; +in float cylindricalVertexDistance; +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 + AnimationPosition * 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), AnimationPosition); + color.rgb += (animationHighlight); + } + #ifdef ALPHA_CUTOUT + if (color.a < ALPHA_CUTOUT) { + discard; + } + #endif + fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, 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..8fcd99f --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh @@ -0,0 +1,23 @@ +#version 150 + +in vec4 vertexColor; +in vec2 texCoord0; + +layout(std140) uniform CutoutRadius { + 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; +} 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..20cefca --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/REUSE.toml @@ -0,0 +1,24 @@ +#SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe> +# +#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"] + +[[annotations]] +path = ["h_plus.png"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["ic22487"] diff --git a/src/main/resources/assets/firmament/textures/cape/firm_static.png b/src/main/resources/assets/firmament/textures/cape/firm_static.png Binary files differnew file mode 100644 index 0000000..b01511c --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/firm_static.png diff --git a/src/main/resources/assets/firmament/textures/cape/firmament_star.png b/src/main/resources/assets/firmament/textures/cape/firmament_star.png Binary files differnew file mode 100644 index 0000000..520d309 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/firmament_star.png diff --git a/src/main/resources/assets/firmament/textures/cape/fsr_static.png b/src/main/resources/assets/firmament/textures/cape/fsr_static.png Binary files differnew file mode 100644 index 0000000..de9cf35 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/fsr_static.png diff --git a/src/main/resources/assets/firmament/textures/cape/h_plus.png b/src/main/resources/assets/firmament/textures/cape/h_plus.png Binary files differnew file mode 100644 index 0000000..974bef7 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/h_plus.png diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_background.png b/src/main/resources/assets/firmament/textures/cape/parallax_background.png Binary files differnew file mode 100644 index 0000000..05ef0fa --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/parallax_background.png diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_template.png b/src/main/resources/assets/firmament/textures/cape/parallax_template.png Binary files differnew file mode 100644 index 0000000..7084c12 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/parallax_template.png diff --git a/src/main/resources/assets/firmament/textures/cape/unpleasant_gradient.png b/src/main/resources/assets/firmament/textures/cape/unpleasant_gradient.png Binary files differnew file mode 100644 index 0000000..da2eb85 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/unpleasant_gradient.png diff --git a/src/main/resources/assets/firmament/textures/gui/circle.png b/src/main/resources/assets/firmament/textures/gui/circle.png Binary files differindex ffd3fab..b5021f8 100644 --- a/src/main/resources/assets/firmament/textures/gui/circle.png +++ b/src/main/resources/assets/firmament/textures/gui/circle.png diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.mcmeta new file mode 100644 index 0000000..55fb892 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 18, + "height": 18, + "border": 4 + } + } +} 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 Binary files differindex 612d2e3..04a90e3 100644 --- a/src/main/resources/assets/firmament/textures/gui/sprites/slot_locked.png +++ b/src/main/resources/assets/firmament/textures/gui/sprites/slot_locked.png 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 Binary files differindex 97dd0ea..c897840 100644 --- 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 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 Binary files differindex 9e66cb5..79e15de 100644 --- a/src/main/resources/assets/firmament/textures/gui/sprites/uuid_locked.png +++ b/src/main/resources/assets/firmament/textures/gui/sprites/uuid_locked.png diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3b988b1..ce8f0bd 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -40,6 +40,9 @@ "modmenu": [ "moe.nea.firmament.compat.modmenu.FirmamentModMenuPlugin" ], + "jade": [ + "moe.nea.firmament.compat.jade.FirmamentJadePlugin" + ], "jarvis": [ "moe.nea.firmament.jarvis.JarvisIntegration" ] @@ -48,16 +51,11 @@ "firmament.mixins.json" ], "depends": { - "fabric": "*", + "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" diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index 7418529..c6795db 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -1,22 +1,47 @@ 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 <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V -accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter - -accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData; -accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V -accessible field net/minecraft/entity/passive/AbstractHorseEntity items Lnet/minecraft/inventory/SimpleInventory; -accessible field net/minecraft/entity/passive/AbstractHorseEntity SADDLED_FLAG I -accessible field net/minecraft/entity/passive/AbstractHorseEntity HORSE_FLAGS Lnet/minecraft/entity/data/TrackedData; -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 - -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 class net/minecraft/client/renderer/RenderType$CompositeRenderType +accessible class net/minecraft/client/renderer/RenderType$CompositeState +accessible class net/minecraft/client/gui/Font$PreparedTextBuilder + +accessible field net/minecraft/client/gui/Gui SCORE_DISPLAY_ORDER Ljava/util/Comparator; + +accessible field net/minecraft/client/multiplayer/ClientPacketListener registryAccess Lnet/minecraft/core/RegistryAccess$Frozen; +accessible method net/minecraft/resources/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/resources/RegistryOps$RegistryInfoLookup;)V +accessible class net/minecraft/resources/RegistryOps$HolderLookupAdapter +accessible class net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl +accessible method net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl <init> (Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/SpriteGetter;)V + +accessible field net/minecraft/world/entity/monster/Creeper DATA_IS_POWERED Lnet/minecraft/network/syncher/EntityDataAccessor; +accessible method net/minecraft/world/entity/decoration/ArmorStand setSmall (Z)V +accessible method net/minecraft/server/packs/resources/FallbackResourceManager parseMetadata (Lnet/minecraft/server/packs/resources/IoSupplier;)Lnet/minecraft/server/packs/resources/ResourceMetadata; +accessible method net/minecraft/client/gui/GuiGraphics innerBlit (Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/ResourceLocation;IIIIFFFFI)V + +accessible field net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket MAX_PAYLOAD_SIZE I + +accessible class net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition +accessible field net/minecraft/client/resources/model/BlockStateModelLoader BLOCKSTATE_LISTER Lnet/minecraft/resources/FileToIdConverter; +accessible method net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition <init> (Ljava/lang/String;Lnet/minecraft/client/renderer/block/model/BlockModelDefinition;)V +accessible method net/minecraft/client/resources/model/BlockStateModelLoader loadBlockStateDefinitionStack (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/level/block/state/StateDefinition;Ljava/util/List;)Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedModels; + +mutable field net/minecraft/world/inventory/Slot x I +mutable field net/minecraft/world/inventory/Slot y I + +#accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData; +accessible field net/minecraft/client/renderer/LevelRenderer viewArea Lnet/minecraft/client/renderer/ViewArea; +accessible field net/minecraft/client/renderer/texture/OverlayTexture texture Lnet/minecraft/client/renderer/texture/DynamicTexture; + +accessible method net/minecraft/client/renderer/RenderStateShard$TextureStateShard cutoutTexture ()Ljava/util/Optional; +# was accessible method net/minecraft/client/renderer/RenderStateShard$TextureStateShard getId ()Ljava/util/Optional; + +accessible field net/minecraft/client/renderer/RenderType$CompositeRenderType state Lnet/minecraft/client/renderer/RenderType$CompositeState; +accessible field net/minecraft/client/renderer/RenderType$CompositeState textureState Lnet/minecraft/client/renderer/RenderStateShard$EmptyTextureStateShard; +accessible field net/minecraft/client/multiplayer/MultiPlayerGameMode destroyBlockPos Lnet/minecraft/core/BlockPos; +accessible field net/minecraft/client/renderer/RenderType$CompositeRenderType renderPipeline Lcom/mojang/blaze3d/pipeline/RenderPipeline; + +mutable field net/minecraft/client/renderer/entity/state/LivingEntityRenderState headItem Lnet/minecraft/client/renderer/item/ItemStackRenderState; + +accessible field net/minecraft/world/entity/Entity level Lnet/minecraft/world/level/Level; +accessible field net/minecraft/client/Minecraft userApiService Lcom/mojang/authlib/minecraft/UserApiService; +accessible field net/minecraft/world/entity/Entity position Lnet/minecraft/world/phys/Vec3; +accessible method net/minecraft/world/item/component/ResolvableProfile unpack ()Lcom/mojang/datafixers/util/Either; +accessible field net/minecraft/world/entity/Avatar DATA_PLAYER_MODE_CUSTOMISATION Lnet/minecraft/network/syncher/EntityDataAccessor; diff --git a/src/main/resources/firmament.mixins.json b/src/main/resources/firmament.mixins.json index d78d124..c3392a6 100644 --- a/src/main/resources/firmament.mixins.json +++ b/src/main/resources/firmament.mixins.json @@ -4,6 +4,9 @@ "package": "moe.nea.firmament.mixins", "compatibilityLevel": "JAVA_21", "refmap": "Firmament-refmap.json", + "mixinextras": { + "minVersion": "0.5.0" + }, "injectors": { "defaultRequire": 1 } 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 Binary files differnew file mode 100644 index 0000000..46c86f4 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png 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 Binary files differnew file mode 100644 index 0000000..1831ef3 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png 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 Binary files differnew file mode 100644 index 0000000..5b774b2 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png 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 Binary files differnew file mode 100644 index 0000000..10d41dd --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png 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 Binary files differnew file mode 100644 index 0000000..61e9ee5 --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png 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 Binary files differnew file mode 100644 index 0000000..653a99e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png 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..66928ce --- /dev/null +++ b/src/test/kotlin/MixinTest.kt @@ -0,0 +1,18 @@ +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 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..31fc3f7 --- /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 com.mojang.blaze3d.platform.InputConstants +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.keyWithoutMods(InputConstants.KEY_A) + val bPress = SavedKeyBinding.keyWithoutMods(InputConstants.KEY_B) + val cPress = SavedKeyBinding.keyWithoutMods(InputConstants.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 index 045fdd5..74549e1 100644 --- a/src/test/kotlin/root.kt +++ b/src/test/kotlin/root.kt @@ -1,6 +1,6 @@ package moe.nea.firmament.test -import net.minecraft.Bootstrap +import net.minecraft.server.Bootstrap import net.minecraft.SharedConstants import moe.nea.firmament.util.TimeMark @@ -12,8 +12,8 @@ object FirmTestBootstrap { } init { - SharedConstants.createGameVersion() - Bootstrap.initialize() + SharedConstants.tryDetectVersion() + Bootstrap.bootStrap() } val loadEnd = TimeMark.now() @@ -24,6 +24,7 @@ object FirmTestBootstrap { 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 index 107b565..5f08847 100644 --- a/src/test/kotlin/testutil/ItemResources.kt +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -1,15 +1,24 @@ package moe.nea.firmament.test.testutil -import net.minecraft.item.ItemStack -import net.minecraft.nbt.NbtCompound -import net.minecraft.nbt.NbtElement +import com.mojang.datafixers.DSL +import com.mojang.serialization.Dynamic +import com.mojang.serialization.JsonOps +import net.minecraft.SharedConstants +import net.minecraft.util.datafix.DataFixers +import net.minecraft.util.datafix.fixes.References +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.StringNbtReader -import net.minecraft.registry.RegistryOps -import net.minecraft.text.Text -import net.minecraft.text.TextCodecs +import net.minecraft.nbt.StringTag +import net.minecraft.nbt.TagParser +import net.minecraft.resources.RegistryOps +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +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 { @@ -23,19 +32,63 @@ object ItemResources { .readAllBytes().decodeToString() } - fun loadSNbt(path: String): NbtCompound { - return StringNbtReader.parse(loadString(path)) + fun loadSNbt(path: String): CompoundTag { + return TagParser.parseCompoundFully(loadString(path)) } - fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE) - fun loadText(name: String): Text { - return TextCodecs.CODEC.parse(getNbtOps(), loadSNbt("testdata/chat/$name.snbt")) - .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } + fun getNbtOps(): RegistryOps<Tag> = MC.currentOrDefaultRegistries.createSerializationContext(NbtOps.INSTANCE) + + fun tryMigrateNbt( + nbtCompound: CompoundTag, + typ: DSL.TypeReference?, + ): Tag { + val source = nbtCompound.read("source", ExportedTestConstantMeta.CODEC) + nbtCompound.remove("source") + if (source.isPresent) { + val wrappedNbtSource = if (typ == References.TEXT_COMPONENT && source.get().dataVersion < 4325) { + // Per 1.21.5 text components are wrapped in a string, which firmament unwrapped in the snbt files + StringTag.valueOf( + NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, nbtCompound) + .toString() + ) + } else { + nbtCompound + } + if (typ != null) { + return DataFixers.getDataFixer() + .update( + typ, + Dynamic(NbtOps.INSTANCE, wrappedNbtSource), + source.get().dataVersion, + SharedConstants.getCurrentVersion().dataVersion().version + ).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): Component { + return ComponentSerialization.CODEC.parse( + getNbtOps(), + tryMigrateNbt(loadSNbt("testdata/chat/$name.snbt"), References.TEXT_COMPONENT) + ).getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } } fun loadItem(name: String): ItemStack { - // TODO: make the load work with enchantments - return ItemStack.CODEC.parse(getNbtOps(), loadSNbt("testdata/items/$name.snbt")) - .getOrThrow { IllegalStateException("Could not load test item '$name': $it") } + try { + val itemNbt = loadSNbt("testdata/items/$name.snbt") + return ItemStack.CODEC.parse(getNbtOps(), tryMigrateNbt(itemNbt, References.ITEM_STACK)).orThrow + } catch (ex: Exception) { + throw RuntimeException("Could not load item resource '$name'", ex) + } } } diff --git a/src/test/kotlin/testutil/KotestPlugin.kt b/src/test/kotlin/testutil/KotestPlugin.kt deleted file mode 100644 index 6db50fb..0000000 --- a/src/test/kotlin/testutil/KotestPlugin.kt +++ /dev/null @@ -1,16 +0,0 @@ -package moe.nea.firmament.test.testutil - -import io.kotest.core.config.AbstractProjectConfig -import io.kotest.core.extensions.Extension -import moe.nea.firmament.test.FirmTestBootstrap - -class KotestPlugin : AbstractProjectConfig() { - override fun extensions(): List<Extension> { - return listOf() - } - - override suspend fun beforeProject() { - FirmTestBootstrap.bootstrapMinecraft() - super.beforeProject() - } -} diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt index 949749e..7c581c5 100644 --- a/src/test/kotlin/util/ColorCodeTest.kt +++ b/src/test/kotlin/util/ColorCodeTest.kt @@ -1,57 +1,57 @@ package moe.nea.firmament.test.util -import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test import moe.nea.firmament.util.removeColorCodes -class ColorCodeTest : AnnotationSpec() { - @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() - ) - } +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 index 46ed3b4..94ab222 100644 --- a/src/test/kotlin/util/TextUtilText.kt +++ b/src/test/kotlin/util/TextUtilText.kt @@ -1,16 +1,18 @@ package moe.nea.firmament.test.util -import io.kotest.core.spec.style.AnnotationSpec 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 : AnnotationSpec() { +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()) + 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 <T> assertEqualCycles( + expected: List<T>, + actual: List<T> + ) { + 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..dc06e4b --- /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.world.phys.Vec2 +import moe.nea.firmament.util.math.Projections + +class ProjectionsBoxTest { + val Double.degrees get() = Math.toRadians(this) + + @TestFactory + fun testProjections(): Stream<DynamicTest> { + return sequenceOf( + 0.0.degrees to Vec2(1F, 0F), + 63.4349.degrees to Vec2(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 index 206a357..43a3212 100644 --- a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -1,18 +1,18 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.AnnotationSpec 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 net.minecraft.network.chat.Component import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.AbilityUtils import moe.nea.firmament.util.unformattedString -class AbilityUtilsTest : AnnotationSpec() { +class AbilityUtilsTest { fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map { - it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) }) + it.copy(descriptionLines = it.descriptionLines.map { Component.literal(it.unformattedString) }) } @Test @@ -24,9 +24,11 @@ class AbilityUtilsTest : AnnotationSpec() { 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), + listOf( + "Throw your pickaxe to create an", + "explosion mining all ores in a 3 block", + "radius." + ).map(Component::literal), 48.seconds ) ), @@ -43,8 +45,10 @@ class AbilityUtilsTest : AnnotationSpec() { true, AbilityUtils.AbilityActivation.RIGHT_CLICK, null, - listOf("Grants +200% ⸕ Mining Speed for", - "10s.").map(Text::literal), + listOf( + "Grants +200% ⸕ Mining Speed for", + "10s." + ).map(Component::literal), 2.minutes ) ), @@ -58,8 +62,10 @@ class AbilityUtilsTest : AnnotationSpec() { 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), + listOf( + "Teleport 12 blocks ahead of you and", + "gain +50 ✦ Speed for 3 seconds." + ).map(Component::literal), null ), AbilityUtils.ItemAbility( @@ -67,9 +73,11 @@ class AbilityUtilsTest : AnnotationSpec() { false, AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK, 90, - listOf("Teleport to your targeted block up", - "to 61 blocks away.", - "Soulflow Cost: 1").map(Text::literal), + listOf( + "Teleport to your targeted block up", + "to 61 blocks away.", + "Soulflow Cost: 1" + ).map(Component::literal), null ) ), diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt index cca3d13..c0ef2a3 100644 --- a/src/test/kotlin/util/skyblock/ItemTypeTest.kt +++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt @@ -1,26 +1,28 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.ShouldSpec -import io.kotest.matchers.shouldBe +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 - : ShouldSpec( - { - context("ItemType.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, - ).forEach { (name, typ) -> - should("return $typ for $name") { - ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ - } +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 index f93cd2b..e0e3e63 100644 --- a/src/test/kotlin/util/skyblock/SackUtilTest.kt +++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt @@ -1,12 +1,12 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.AnnotationSpec 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 : AnnotationSpec() { +class SackUtilTest { @Test fun testOneRottenFlesh() { Assertions.assertEquals( 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 index 15cc2de..386194b 100644 --- a/src/test/resources/testdata/chat/all-chat.snbt +++ b/src/test/resources/testdata/chat/all-chat.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, extra: [ { bold: 0b, 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 index 924a558..d7b8b90 100644 --- a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt +++ b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, color: "#FFAA00", extra: [ { diff --git a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt index 924a558..d7b8b90 100644 --- a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt +++ b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, color: "#FFAA00", extra: [ { diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt index 180c069..9ffd385 100644 --- a/src/test/resources/testdata/items/aspect-of-the-void.snbt +++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ 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 index cce12f9..aa5e590 100644 --- a/src/test/resources/testdata/items/diamond-pickaxe.snbt +++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/gemstone-gauntlet.snbt b/src/test/resources/testdata/items/gemstone-gauntlet.snbt index 92ce739..92bb806 100644 --- a/src/test/resources/testdata/items/gemstone-gauntlet.snbt +++ b/src/test/resources/testdata/items/gemstone-gauntlet.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt index c57d457..f0025b9 100644 --- a/src/test/resources/testdata/items/hyperion.snbt +++ b/src/test/resources/testdata/items/hyperion.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt index b73542d..875047d 100644 --- a/src/test/resources/testdata/items/implosion-belt.snbt +++ b/src/test/resources/testdata/items/implosion-belt.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ 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 index 6e92685..c364032 100644 --- a/src/test/resources/testdata/items/pets/lion-item.snbt +++ b/src/test/resources/testdata/items/pets/lion-item.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ 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 index c0ef585..79f32c9 100644 --- a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt +++ b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { id: "PET", diff --git a/src/test/resources/testdata/items/pets/rabbit-selected.snbt b/src/test/resources/testdata/items/pets/rabbit-selected.snbt index 48a6f6f..d4c7235 100644 --- a/src/test/resources/testdata/items/pets/rabbit-selected.snbt +++ b/src/test/resources/testdata/items/pets/rabbit-selected.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { id: "PET", diff --git a/src/test/resources/testdata/items/rune-in-sack.snbt b/src/test/resources/testdata/items/rune-in-sack.snbt index b15488a..4624c0f 100644 --- a/src/test/resources/testdata/items/rune-in-sack.snbt +++ b/src/test/resources/testdata/items/rune-in-sack.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:custom_data": { }, diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt index e3b6819..e49c6b0 100644 --- a/src/test/resources/testdata/items/titanium-drill.snbt +++ b/src/test/resources/testdata/items/titanium-drill.snbt @@ -1,4 +1,7 @@ { + source: { + dataVersion: 4189, + }, components: { "minecraft:attribute_modifiers": { modifiers: [ 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/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 index dc3b109..bc0f36a 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -2,7 +2,12 @@ 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 @@ -17,23 +22,37 @@ 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.client.render.model.BakedModel -import net.minecraft.client.util.ModelIdentifier -import net.minecraft.registry.RegistryKey -import net.minecraft.registry.RegistryKeys -import net.minecraft.resource.ResourceManager -import net.minecraft.resource.SinglePreparationResourceReloader -import net.minecraft.util.Identifier -import net.minecraft.util.math.BlockPos -import net.minecraft.util.profiler.Profiler +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.Blocks +import net.minecraft.client.resources.model.ModelBaker +import net.minecraft.client.renderer.block.model.BlockStateModel +import net.minecraft.client.resources.model.BlockStateModelLoader +import net.minecraft.client.resources.model.ModelDiscovery +import net.minecraft.client.renderer.block.model.SingleVariant +import net.minecraft.client.renderer.block.model.BlockModelDefinition +import net.minecraft.client.renderer.block.model.Variant +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.server.packs.resources.Resource +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.world.level.block.state.StateDefinition +import net.minecraft.resources.ResourceLocation +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.AABB +import net.minecraft.util.profiling.ProfilerFiller +import net.minecraft.util.thread.ParallelMapTransform 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.debug.DebugLogger +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 @@ -43,249 +62,406 @@ import moe.nea.firmament.util.json.SingletonSerializableList object CustomBlockTextures { - @Serializable - data class CustomBlockOverride( - val modes: @Serializable(SingletonSerializableList::class) List<String>, - val area: List<Area>? = null, - val replacements: Map<Identifier, Replacement>, - ) - - @Serializable(with = Replacement.Serializer::class) - data class Replacement( - val block: Identifier, - val sound: Identifier?, - ) { - - @Transient - val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament") - - @Transient - val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) { - MC.instance.bakedModelManager.getModel(blockModelIdentifier) - } - - @OptIn(ExperimentalSerializationApi::class) - @kotlinx.serialization.Serializer(Replacement::class) - object DefaultSerializer : KSerializer<Replacement> - - object Serializer : KSerializer<Replacement> { - val delegate = serializer<JsonElement>() - 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( + @Serializable + data class CustomBlockOverride( + val modes: @Serializable(SingletonSerializableList::class) List<String>, + val area: List<Area>? = null, + val replacements: Map<ResourceLocation, Replacement>, + ) + + @Serializable(with = Replacement.Serializer::class) + data class Replacement( + val block: ResourceLocation, + val sound: ResourceLocation?, + ) { + fun replace(block: BlockState): BlockStateModel? { + blockStateMap?.let { return it[block] } + return blockModel + } + + @Transient + lateinit var overridingBlock: Block + + @Transient + val blockModelIdentifier get() = block.withPrefix("block/") + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete, if [unbakedBlockStateMap] is set. + */ + @Transient + var blockStateMap: Map<BlockState, BlockStateModel>? = null + + @Transient + var unbakedBlockStateMap: Map<BlockState, BlockStateModel.UnbakedRoot>? = 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<Replacement> + + object Serializer : KSerializer<Replacement> { + val delegate = serializer<JsonElement>() + 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(ResourceLocation.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<Block, List<BlockReplacement>> - ) - - data class BlockReplacement( - val checks: List<Area>?, - 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<SkyBlockIsland, LocationReplacements>) - - 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?): BakedModel? { - return getReplacement(block, blockPos)?.bakedModel - } - - @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 - var preparationFuture: CompletableFuture<BakedReplacements> = 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<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>() - for ((file, resource) in resources) { - val json = - Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(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) }) - } - - @JvmStatic - fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel { - return getReplacementModel(state, pos) ?: orig - } - - @Subscribe - fun onStart(event: FinalizeResourceManagerEvent) { - event.resourceManager.registerReloader(object : - SinglePreparationResourceReloader<BakedReplacements>() { - override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements { - return preparationFuture.join() - } - - override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) { - allLocationReplacements = prepared - refreshReplacements() - } - }) - } + ) { + @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) + } + + fun toBox(): AABB { + return AABB( + realMin.x.toDouble(), + realMin.y.toDouble(), + realMin.z.toDouble(), + (realMax.x + 1).toDouble(), + (realMax.y + 1).toDouble(), + (realMax.z + 1).toDouble() + ) + } + } + + data class LocationReplacements( + val lookup: Map<Block, List<BlockReplacement>> + ) { + init { + lookup.forEach { (block, replacements) -> + for (replacement in replacements) { + replacement.replacement.overridingBlock = block + } + } + } + } + + data class BlockReplacement( + val checks: List<Area>?, + 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<SkyBlockIsland, LocationReplacements>) { + /** + * Fulfilled by [createBakedModels] which is called during model baking. Once completed, all [Replacement.blockModel] will be set. + */ + val modelBakingFuture = CompletableFuture<Unit>() + + /** + * @returns a list of all [Replacement]s. + */ + fun collectAllReplacements(): Sequence<Replacement> { + 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.viewArea?.sections?.forEach { + // false schedules rebuilds outside a 27 block radius to happen async + // nb: this sets the dirty but to true, the boolean parameter specifies the update behaviour + it.setDirty(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<BakedReplacements> = 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.listResources("overrides/blocks") { + it.namespace == "firmskyblock" && it.path.endsWith(".json") + } + val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>() + for ((file, resource) in resources) { + val json = + Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.open()) + .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.lookupOrThrow(Registries.BLOCK) + .get(ResourceKey.create(Registries.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.registerReloadListener(object : + SimplePreparableReloadListener<BakedReplacements>() { + override fun prepare(manager: ResourceManager, profiler: ProfilerFiller): BakedReplacements { + return preparationFuture.join().also { + it.modelBakingFuture.join() + } + } + + override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: ProfilerFiller?) { + allLocationReplacements = prepared + refreshReplacements() + } + }) + } + + fun simpleBlockModel(blockId: ResourceLocation): SingleVariant.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 SingleVariant.Unbaked( + Variant(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: ModelDiscovery) { + preparationFuture.join().collectAllReplacements() + .forEach { + modelsCollector.addRoot(simpleBlockModel(it.blockModelIdentifier)) + it.unbakedBlockStateMap?.values?.forEach { + modelsCollector.addRoot(it) + } + } + } + + @JvmStatic + fun createBakedModels(baker: ModelBaker, executor: Executor): CompletableFuture<Void?> { + 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 = ParallelMapTransform.schedule(byModel, { blockId, replacements -> + val unbakedModel = SingleVariant.Unbaked( + Variant(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<ResourceLocation, List<Resource>>, + stateManagers: Function<ResourceLocation, StateDefinition<Block, BlockState>?> + ) { + extra.collectAllReplacements().forEach { + val blockId = BuiltInRegistries.BLOCK.getResourceKey(it.overridingBlock).getOrNull()?.location() ?: return@forEach + val allModels = mutableListOf<BlockStateModelLoader.LoadedBlockModelDefinition>() + val stateManager = stateManagers.apply(blockId) ?: return@forEach + for (resource in original[BlockStateModelLoader.BLOCKSTATE_LISTER.idToFile(it.block)] ?: return@forEach) { + try { + resource.openAsReader().use { reader -> + val jsonElement = JsonParser.parseReader(reader) + val blockModelDefinition = + BlockModelDefinition.CODEC.parse(JsonOps.INSTANCE, jsonElement) + .getOrThrow { msg: String? -> JsonParseException(msg) } + allModels.add( + BlockStateModelLoader.LoadedBlockModelDefinition( + resource.sourcePackId(), + blockModelDefinition + ) + ) + } + } catch (exception: Exception) { + ErrorUtil.softError( + "Failed to load custom blockstate definition ${it.block} from pack ${resource.sourcePackId()}", + exception + ) + } + } + + try { + it.unbakedBlockStateMap = BlockStateModelLoader.loadBlockStateDefinitionStack( + 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/CustomBlockTexturesDebugger.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTexturesDebugger.kt new file mode 100644 index 0000000..ba19e0f --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTexturesDebugger.kt @@ -0,0 +1,132 @@ +package moe.nea.firmament.features.texturepack + +import com.mojang.brigadier.arguments.IntegerArgumentType +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager +import net.minecraft.world.level.block.Block +import net.minecraft.commands.arguments.blocks.BlockStateParser +import net.minecraft.commands.arguments.blocks.BlockStateArgument +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 +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.WorldRenderLastEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.tr + +object CustomBlockTexturesDebugger { + var debugMode: DebugMode = DebugMode.Never + var range = 30 + + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + if (debugMode == DebugMode.Never) return + val replacements = CustomBlockTextures.currentIslandReplacements ?: return + RenderInWorldContext.renderInWorld(event) { + for ((block, repl) in replacements.lookup) { + if (!debugMode.shouldHighlight(block)) continue + for (i in repl) { + if (i.roughCheck != null) + tryRenderBox(i.roughCheck!!.toBox(), 0x50FF8050.toInt()) + i.checks?.forEach { area -> + tryRenderBox(area.toBox(), 0x5050FF50.toInt()) + } + } + } + } + } + + fun RenderInWorldContext.tryRenderBox(box: AABB, colour: Int) { + val player = MC.player?.position ?: Vec3.ZERO + if (box.center.distanceTo(player) < range + maxOf( + box.zsize, box.xsize, box.ysize + ) / 2 && !box.contains(player) + ) { + box(box, colour) + } + } + + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("debugcbt") { + thenLiteral("range") { + thenArgument("range", IntegerArgumentType.integer(0)) { rangeArg -> + thenExecute { + this@CustomBlockTexturesDebugger.range = get(rangeArg) + MC.sendChat( + tr( + "firmament.debugcbt.always", + "Only render areas within ${this@CustomBlockTexturesDebugger.range} blocks" + ) + ) + } + } + } + thenLiteral("all") { + thenExecute { + debugMode = DebugMode.Always + MC.sendChat( + tr( + "firmament.debugcbt.always", + "Showing debug outlines for all custom block textures" + ) + ) + } + } + thenArgument("block", BlockStateArgument.block(event.commandRegistryAccess)) { block -> + thenExecute { + val block = get(block).state.block + debugMode = DebugMode.ForBlock(block) + MC.sendChat( + tr( + "firmament.debugcbt.block", + "Showing debug outlines for all custom ${block.name} textures" + ) + ) + } + } + thenLiteral("never") { + thenExecute { + debugMode = DebugMode.Never + MC.sendChat( + tr( + "firmament.debugcbt.disabled", + "Disabled debug outlines for custom block textures" + ) + ) + } + } + } + } + } + + sealed interface DebugMode { + fun shouldHighlight(block: Block): Boolean + + data object Never : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return false + } + } + + data class ForBlock(val block: Block) : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return block == this.block + } + } + + data object Always : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return true + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt index 85dfa32..7e3c018 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt @@ -8,16 +8,16 @@ 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 net.minecraft.client.resources.model.EquipmentClientInfo +import net.minecraft.world.item.equipment.Equippable +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.equipment.EquipmentAssets +import net.minecraft.resources.ResourceKey +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FinalizeResourceManagerEvent @@ -30,14 +30,14 @@ import moe.nea.firmament.util.skyBlockId object CustomGlobalArmorOverrides { @Serializable data class ArmorOverride( - @SerialName("item_ids") + @SerialName("item_ids") val itemIds: List<String>, - val layers: List<ArmorOverrideLayer>? = null, - val model: Identifier? = null, - val overrides: List<ArmorOverrideOverride> = listOf(), + val layers: List<ArmorOverrideLayer>? = null, + val model: ResourceLocation? = null, + val overrides: List<ArmorOverrideOverride> = listOf(), ) { @Transient - lateinit var modelIdentifier: Identifier + lateinit var modelIdentifier: ResourceLocation fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) overrides.forEach { it.bake(manager) } @@ -51,16 +51,16 @@ object CustomGlobalArmorOverrides { @Serializable data class ArmorOverrideLayer( - val tint: Boolean = false, - val identifier: Identifier, - val suffix: String = "", + val tint: Boolean = false, + val identifier: ResourceLocation, + val suffix: String = "", ) @Serializable data class ArmorOverrideOverride( - val predicate: FirmamentModelPredicate, - val layers: List<ArmorOverrideLayer>? = null, - val model: Identifier? = null, + val predicate: FirmamentModelPredicate, + val layers: List<ArmorOverrideLayer>? = null, + val model: ResourceLocation? = null, ) { init { require(layers != null || model != null) { "Either model or layers must be specified for armor override override" } @@ -68,60 +68,69 @@ object CustomGlobalArmorOverrides { } @Transient - lateinit var modelIdentifier: Identifier + lateinit var modelIdentifier: ResourceLocation fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) } } - private fun resolveComponent(slot: EquipmentSlot, model: Identifier): EquippableComponent { - return EquippableComponent( + private fun resolveComponent(slot: EquipmentSlot, model: ResourceLocation): Equippable { + return Equippable( slot, null, - Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)), + Optional.of(ResourceKey.create(EquipmentAssets.ROOT_ID, model)), Optional.empty(), - Optional.empty(), false, false, false + Optional.empty(), + false, + false, + false, + false, + false, + null ) } + // 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.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot -> - val id = stack.skyBlockId ?: return@memoize Optional.empty() - val override = overrides[id.neuItem] ?: return@memoize Optional.empty() + WeakCache.dontMemoize<ItemStack, EquipmentSlot, Optional<Equippable>>("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@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() } } - return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, override.modelIdentifier).intoOptional() } var overrides: Map<String, ArmorOverride> = mapOf() - private var bakedOverrides: MutableMap<Identifier, EquipmentModel> = mutableMapOf() + private var bakedOverrides: MutableMap<ResourceLocation, EquipmentClientInfo> = mutableMapOf() private val sentinelFirmRunning = AtomicInteger() - private fun bakeModel(model: Identifier?, layers: List<ArmorOverrideLayer>?): Identifier { + private fun bakeModel(model: ResourceLocation?, layers: List<ArmorOverrideLayer>?): ResourceLocation { 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 identifier = ResourceLocation.parse("firmament:sentinel/armor/$idNumber") val equipmentLayers = layers.map { - EquipmentModel.Layer( + EquipmentClientInfo.Layer( it.identifier, if (it.tint) { - Optional.of(EquipmentModel.Dyeable(Optional.empty())) + Optional.of(EquipmentClientInfo.Dyeable(Optional.of(0xFFA06540.toInt()))) } else { Optional.empty() }, false ) } - bakedOverrides[identifier] = EquipmentModel( + bakedOverrides[identifier] = EquipmentClientInfo( mapOf( - EquipmentModel.LayerType.HUMANOID to equipmentLayers, - EquipmentModel.LayerType.HUMANOID_LEGGINGS to equipmentLayers, + EquipmentClientInfo.LayerType.HUMANOID to equipmentLayers, + EquipmentClientInfo.LayerType.HUMANOID_LEGGINGS to equipmentLayers, ) ) return identifier @@ -133,38 +142,40 @@ object CustomGlobalArmorOverrides { @Subscribe fun onStart(event: FinalizeResourceManagerEvent) { - event.resourceManager.registerReloader(object : - SinglePreparationResourceReloader<Map<String, ArmorOverride>>() { - override fun prepare(manager: ResourceManager, profiler: Profiler): Map<String, ArmorOverride> { - val overrideFiles = manager.findResources("overrides/armor_models") { + event.resourceManager.registerReloadListener(object : + SimplePreparableReloadListener<Map<String, ArmorOverride>>() { + override fun prepare(manager: ResourceManager, profiler: ProfilerFiller): Map<String, ArmorOverride> { + val overrideFiles = manager.listResources("overrides/armor_models") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } val overrides = overrideFiles.mapNotNull { - Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex -> + Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.open()).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<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) { - bakedOverrides.clear() + override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: ProfilerFiller) { overrides = prepared } }) } @JvmStatic - fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<EquippableComponent> { + fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<Equippable> { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return Optional.empty() return overrideCache.invoke(itemStack, slot) } @JvmStatic - fun overrideArmorLayer(id: Identifier): EquipmentModel? { + fun overrideArmorLayer(id: ResourceLocation): EquipmentClientInfo? { + 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 index 02c0714..60a6c06 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -8,40 +8,33 @@ import org.slf4j.LoggerFactory import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlin.jvm.optionals.getOrNull -import net.minecraft.client.util.ModelIdentifier -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 net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller 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<CustomGlobalTextures.CustomGuiTextureOverride>(), - SubscriptionOwner { - override val delegateFeature: FirmamentFeature - get() = CustomSkyBlockTextures - +object CustomGlobalTextures : SimplePreparableReloadListener<CustomGlobalTextures.CustomGuiTextureOverride>() { class CustomGuiTextureOverride( val classes: List<ItemOverrideCollection> ) @Serializable data class GlobalItemOverride( - val screen: @Serializable(SingletonSerializableList::class) List<Identifier>, - val model: Identifier, - val predicate: FirmamentModelPredicate, + val screen: @Serializable(SingletonSerializableList::class) List<ResourceLocation>, + val model: ResourceLocation, + val predicate: FirmamentModelPredicate, ) @Serializable @@ -56,7 +49,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText @Subscribe fun onStart(event: FinalizeResourceManagerEvent) { - MC.resourceManager.registerReloader(this) + MC.resourceManager.registerReloadListener(this) } @Subscribe @@ -65,27 +58,29 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText .supplyAsync( { prepare(event.resourceManager) - }, event.preparationExecutor) + }, event.preparationExecutor + ) } @Volatile var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture( - CustomGuiTextureOverride(listOf())) + CustomGuiTextureOverride(listOf()) + ) - override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride { + override fun prepare(manager: ResourceManager?, profiler: ProfilerFiller?): CustomGuiTextureOverride { return preparationFuture.join() } - override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) { + override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: ProfilerFiller?) { 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") } + manager.listResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } .mapNotNull { - Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex -> + Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.open()).getOrElse { ex -> ErrorUtil.softError("Failed to load global item override at ${it.key}", ex) null } @@ -97,15 +92,18 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText .mapNotNull { val key = it.key val guiClassResource = - manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json")) + manager.getResource(ResourceLocation.fromNamespaceAndPath(key.namespace, "filters/screen/${key.path}.json")) .getOrNull() ?: return@mapNotNull runNull { - ErrorUtil.softError("Failed to locate screen filter at $key") + ErrorUtil.softError("Failed to locate screen filter at $key used by ${it.value.map { it.first }}") } val screenFilter = - Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream) + Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.open()) .getOrElse { ex -> - ErrorUtil.softError("Failed to load screen filter at $key", 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 }) @@ -120,7 +118,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText @Subscribe fun onOpenGui(event: ScreenChangeEvent) { - val newTitle = event.new?.title ?: Text.empty() + val newTitle = event.new?.title ?: Component.empty() matchingOverrides = guiClassOverrides.classes .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt index fca8944..2d615c2 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -7,9 +7,9 @@ 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 net.minecraft.client.renderer.item.ItemModels +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FinalizeResourceManagerEvent @@ -17,11 +17,14 @@ 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 { @@ -46,11 +49,11 @@ object CustomModelOverrideParser { } ) - val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>() + val predicateParsers = mutableMapOf<ResourceLocation, FirmamentModelPredicateParser>() fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) { - predicateParsers[Identifier.of("firmament", name)] = parser + predicateParsers[ResourceLocation.fromNamespaceAndPath("firmament", name)] = parser } init { @@ -62,6 +65,8 @@ object CustomModelOverrideParser { 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( @@ -79,8 +84,14 @@ object CustomModelOverrideParser { 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 identifier = ResourceLocation.parse(predicateName) val parser = predicateParsers[identifier] ?: return neverPredicate val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate parsedPredicates.add(parsedPredicate) @@ -99,10 +110,14 @@ object CustomModelOverrideParser { @Subscribe fun finalizeResources(event: FinalizeResourceManagerEvent) { - ItemModelTypes.ID_MAPPER.put( + ItemModels.ID_MAPPER.put( Firmament.identifier("predicates/legacy"), PredicateModel.Unbaked.CODEC ) + ItemModels.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..e026365 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt @@ -0,0 +1,257 @@ +package moe.nea.firmament.features.texturepack + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.client.gui.Font +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.HangingSignEditScreen +import net.minecraft.client.gui.screens.inventory.SignEditScreen +import net.minecraft.client.renderer.RenderType +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller +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.mixins.accessor.AccessorScreenHandler +import moe.nea.firmament.util.ErrorUtil.intoCatch +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.accessors.castAccessor + +object CustomScreenLayouts : SimplePreparableReloadListener<List<CustomScreenLayouts.CustomScreenLayout>>() { + + @Serializable + data class CustomScreenLayout( + val predicates: Preds, + val background: BackgroundReplacer? = null, + val slots: List<SlotReplacer> = listOf(), + val playerTitle: TitleReplacer? = null, + val containerTitle: TitleReplacer? = null, + val repairCostTitle: TitleReplacer? = null, + val nameField: ComponentMover? = null, + val signLines: List<ComponentMover>? = null, + ) { + init { + if (signLines != null) + require(signLines.size == 4) + } + } + + @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: ResourceLocation? = null, + ) { + fun matches(screen: Screen): Boolean { + // TODO: does this deserve the restriction to handled screen + val type = when (screen) { + is AbstractContainerScreen<*> -> (screen.menu as AccessorScreenHandler).type_firmament?.let { + BuiltInRegistries.MENU.getKey(it) + } + + is HangingSignEditScreen -> ResourceLocation.fromNamespaceAndPath("firmskyblock", "hanging_sign") + is SignEditScreen -> ResourceLocation.fromNamespaceAndPath("firmskyblock", "sign") + else -> null + } + val typeMatches = screenType == null || type == screenType; + return label.matches(screen.title) && typeMatches + } + } + + @Serializable + data class BackgroundReplacer( + @Serializable(with = IdentifierSerializer::class) + val texture: ResourceLocation, + // 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 renderDirect(context: GuiGraphics) { + context.blit( + RenderPipelines.GUI_TEXTURED, + this.texture, + this.x, this.y, + 0F, 0F, + this.width, this.height, this.width, this.height, + ) + } + + fun renderGeneric(context: GuiGraphics, screen: AbstractContainerScreen<*>) { + screen.castAccessor() + 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.blit( + RenderPipelines.GUI_TEXTURED, + 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<Slot>) { + 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: Component? = replace?.let(Component::literal) + + fun replaceText(text: Component): Component { + if (replacedText != null) return replacedText + return text + } + + fun replaceY(y: Int): Int { + return this.y ?: y + } + + fun replaceX(font: Font, text: Component, x: Int): Int { + val baseX = this.x ?: x + return baseX + when (this.align) { + LEFT -> 0 + CENTER -> -font.width(text) / 2 + RIGHT -> -font.width(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: Component, color: Int): Int { + return CustomTextColors.mapTextColor(text, color) + } + } + + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloadListener(CustomScreenLayouts) + } + + override fun prepare( + manager: ResourceManager, + profiler: ProfilerFiller + ): List<CustomScreenLayout> { + val allScreenLayouts = manager.listResources( + "overrides/screen_layout", + { it.path.endsWith(".json") && it.namespace == "firmskyblock" }) + val allParsedLayouts = allScreenLayouts.mapNotNull { (path, stream) -> + Firmament.tryDecodeJsonFromStream<CustomScreenLayout>(stream.open()) + .intoCatch("Could not read custom screen layout from $path").orNull() + } + return allParsedLayouts + } + + var customScreenLayouts = listOf<CustomScreenLayout>() + + override fun apply( + prepared: List<CustomScreenLayout>, + manager: ResourceManager?, + profiler: ProfilerFiller? + ) { + this.customScreenLayouts = prepared + } + + @get:JvmStatic + var activeScreenOverride = null as CustomScreenLayout? + + val DO_NOTHING_TEXT_REPLACER = TitleReplacer() + + @JvmStatic + fun <T> getMover(selector: (CustomScreenLayout) -> (T?)) = + activeScreenOverride?.let(selector) + + @JvmStatic + fun getSignTextMover(index: Int) = + getMover { it.signLines?.get(index) } + + @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? AbstractContainerScreen<*> ?: return + val handler = screen.menu + 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 index d9ca5b4..a324402 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -3,29 +3,29 @@ package moe.nea.firmament.features.texturepack import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.properties.Property import java.util.Optional -import org.jetbrains.annotations.Nullable 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 net.minecraft.world.level.block.SkullBlock +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.RenderType +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.resources.ResourceLocation 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.mc.decodeProfileTextureProperty import moe.nea.firmament.util.skyBlockId -object CustomSkyBlockTextures : FirmamentFeature { - override val identifier: String +object CustomSkyBlockTextures { + val identifier: String get() = "custom-skyblock-textures" + @Config 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 } @@ -34,17 +34,17 @@ object CustomSkyBlockTextures : FirmamentFeature { 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, - CustomGlobalArmorOverrides.overrideCache.cache + CustomItemModelEvent.cache.cache, + // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache ) } @@ -75,13 +75,13 @@ object CustomSkyBlockTextures : FirmamentFeature { fun onCustomModelId(it: CustomItemModelEvent) { if (!TConfig.enabled) return val id = it.itemStack.skyBlockId ?: return - it.overrideIfExists(Identifier.of("firmskyblock", id.identifier.path)) + it.overrideIfEmpty(ResourceLocation.fromNamespaceAndPath("firmskyblock", id.identifier.path)) } private val skullTextureCache = - WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component -> + WeakCache.memoize<ResolvableProfile, Optional<ResourceLocation>>("SkullTextureCache") { component -> val id = getSkullTexture(component) ?: return@memoize Optional.empty() - if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { + if (!Minecraft.getInstance().resourceManager.getResource(id).isPresent) { return@memoize Optional.empty() } return@memoize Optional.of(id) @@ -97,21 +97,21 @@ object CustomSkyBlockTextures : FirmamentFeature { 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 getSkullTexture(profile: ResolvableProfile): ResourceLocation? { + val id = getSkullId(profile.partialProfile().properties["textures"].firstOrNull() ?: return null) ?: return null + return ResourceLocation.fromNamespaceAndPath("firmskyblock", "textures/placedskull/$id.png") } fun modifySkullTexture( - type: SkullBlock.SkullType?, - component: ProfileComponent?, - cir: CallbackInfoReturnable<RenderLayer> + type: SkullBlock.Type?, + component: ResolvableProfile?, + cir: CallbackInfoReturnable<RenderType> ) { - if (type != SkullBlock.Type.PLAYER) return + if (type != SkullBlock.Types.PLAYER) return if (!TConfig.skullsEnabled) return if (component == null) return val n = skullTextureCache.invoke(component).getOrNull() ?: return - cir.returnValue = RenderLayer.getEntityTranslucent(n) + cir.returnValue = RenderType.entityTranslucent(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 index 4ca1796..5f3b08d 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt @@ -2,52 +2,66 @@ 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 net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller 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<CustomTextColors.TextOverrides?>() { +object CustomTextColors : SimplePreparableReloadListener<CustomTextColors.TextOverrides?>() { @Serializable data class TextOverrides( val defaultColor: Int, val overrides: List<TextOverride> = 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) + event.resourceManager.registerReloadListener(this) } - val cache = WeakCache.memoize<Text, Optional<Int>>("CustomTextColor") { text -> + val cache = WeakCache.memoize<Component, Optional<TextOverride>>("CustomTextColor") { text -> val override = textOverrides ?: return@memoize Optional.empty() - Optional.of(override.overrides.find { it.predicate.matches(text) }?.override ?: override.defaultColor) + Optional.ofNullable(override.overrides.find { it.predicate.matches(text) }) } - fun mapTextColor(text: Text, oldColor: Int): Int { - if (textOverrides == null) return oldColor - return cache(text).getOrNull() ?: oldColor + fun mapTextColor(text: Component, oldColor: Int): Int { + val override = cache(text).orElse(null) + return override?.override ?: textOverrides?.defaultColor ?: oldColor } override fun prepare( - manager: ResourceManager, - profiler: Profiler + manager: ResourceManager, + profiler: ProfilerFiller ): TextOverrides? { - val resource = manager.getResource(Identifier.of("firmskyblock", "overrides/text_colors.json")).getOrNull() + val resource = manager.getResource(ResourceLocation.fromNamespaceAndPath("firmskyblock", "overrides/text_colors.json")).getOrNull() ?: return null - return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.inputStream) + return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.open()) .getOrElse { Firmament.logger.error("Could not parse text_colors.json", it) null @@ -57,9 +71,9 @@ object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.Tex var textOverrides: TextOverrides? = null override fun apply( - prepared: TextOverrides?, - manager: ResourceManager, - profiler: Profiler + prepared: TextOverrides?, + manager: ResourceManager, + profiler: ProfilerFiller ) { textOverrides = prepared } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt new file mode 100644 index 0000000..71617fd --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt @@ -0,0 +1,56 @@ +package moe.nea.firmament.features.texturepack + +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.util.profiling.ProfilerFiller +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.util.ErrorUtil.intoCatch + +object CustomTextReplacements : SimplePreparableReloadListener<List<TreeishTextReplacer>>() { + + override fun prepare( + manager: ResourceManager, + profiler: ProfilerFiller + ): List<TreeishTextReplacer> { + return manager.listResources("overrides/texts") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } + .mapNotNull { + Firmament.tryDecodeJsonFromStream<TreeishTextReplacer>(it.value.open()) + .intoCatch("Failed to load text override from ${it.key}").orNull() + } + } + + var textReplacers: List<TreeishTextReplacer> = listOf() + + override fun apply( + prepared: List<TreeishTextReplacer>, + manager: ResourceManager, + profiler: ProfilerFiller + ) { + this.textReplacers = prepared + } + + @JvmStatic + fun replaceTexts(texts: List<Component>): List<Component> { + return texts.map { replaceText(it) } + } + + @JvmStatic + fun replaceText(text: Component): Component { + // TODO: add a config option for this + val rawText = text.string + var text = text + for (replacer in textReplacers) { + if (!replacer.match.matches(rawText)) continue + text = replacer.replaceText(text) + } + return text + } + + @Subscribe + fun onReloadStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloadListener(this) + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt index d11fec0..d10b6e9 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt @@ -1,8 +1,11 @@ - package moe.nea.firmament.features.texturepack -import net.minecraft.item.ItemStack +import kotlinx.serialization.Serializable +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +@Serializable(with = FirmamentRootPredicateSerializer::class) interface FirmamentModelPredicate { - fun test(stack: ItemStack): Boolean + 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/FirmamentRootPredicateSerializer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt index 0b8ae8e..39e1fc3 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import moe.nea.firmament.features.texturepack.predicates.AndPredicate +import moe.nea.firmament.util.json.intoGson object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> { val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() 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..03496aa --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt @@ -0,0 +1,92 @@ +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.renderer.item.ItemModelResolver +import net.minecraft.client.renderer.item.ItemStackRenderState +import net.minecraft.client.renderer.item.BlockModelWrapper +import net.minecraft.client.renderer.item.ItemModel +import net.minecraft.client.renderer.item.ItemModels +import net.minecraft.client.resources.model.ResolvableModel +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.ItemStack +import net.minecraft.world.entity.ItemOwner +import net.minecraft.resources.ResourceLocation + +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: ItemStackRenderState) = state as HasExplicitHeadModelMarker + } + } + + data class Baked(val head: ItemModel, val regular: ItemModel) : ItemModel { + + override fun update( + state: ItemStackRenderState, + stack: ItemStack?, + resolver: ItemModelResolver?, + displayContext: ItemDisplayContext, + world: ClientLevel?, + heldItemContext: ItemOwner?, + 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, heldItemContext, seed) + } + } + + data class Unbaked( + val head: ItemModel.Unbaked, + val regular: ItemModel.Unbaked, + ) : ItemModel.Unbaked { + override fun type(): MapCodec<out ItemModel.Unbaked> { + return CODEC + } + + override fun bake(context: ItemModel.BakingContext): ItemModel { + return Baked( + head.bake(context), + regular.bake(context) + ) + } + + override fun resolveDependencies(resolver: ResolvableModel.Resolver) { + head.resolveDependencies(resolver) + regular.resolveDependencies(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 = BlockModelWrapper.Unbaked(ResourceLocation.parse(modelUrl), listOf()) + return Unbaked(headModel, unbakedModel) + } + + val CODEC = RecordCodecBuilder.mapCodec { + it.group( + ItemModels.CODEC.fieldOf("head") + .forGetter(Unbaked::head), + ItemModels.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 index b52e96b..e53c9c7 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt @@ -4,51 +4,51 @@ 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.item.tint.TintSource -import net.minecraft.client.render.model.ResolvableModel -import net.minecraft.client.world.ClientWorld -import net.minecraft.entity.LivingEntity -import net.minecraft.item.ItemStack -import net.minecraft.item.ModelTransformationMode -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.item.ItemModelResolver +import net.minecraft.client.renderer.item.ItemStackRenderState +import net.minecraft.client.renderer.item.BlockModelWrapper +import net.minecraft.client.renderer.item.ItemModel +import net.minecraft.client.renderer.item.ItemModels +import net.minecraft.client.resources.model.ResolvableModel +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.ItemStack +import net.minecraft.world.entity.ItemOwner +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.features.texturepack.predicates.AndPredicate class PredicateModel { data class Baked( - val fallback: ItemModel, - val overrides: List<Override> + val fallback: ItemModel, + val overrides: List<Override> ) : ItemModel { data class Override( - val model: ItemModel, - val predicate: FirmamentModelPredicate, + val model: ItemModel, + val predicate: FirmamentModelPredicate, ) override fun update( - state: ItemRenderState, - stack: ItemStack, - resolver: ItemModelManager, - transformationMode: ModelTransformationMode, - world: ClientWorld?, - user: LivingEntity?, - seed: Int + state: ItemStackRenderState?, + stack: ItemStack, + resolver: ItemModelResolver?, + displayContext: ItemDisplayContext?, + world: ClientLevel?, + heldItemContext: ItemOwner?, + seed: Int ) { val model = overrides - .find { it.predicate.test(stack) } + .findLast { it.predicate.test(stack, heldItemContext?.asLivingEntity()) } ?.model ?: fallback - model.update(state, stack, resolver, transformationMode, world, user, seed) + model.update(state, stack, resolver, displayContext, world, heldItemContext, seed) } } data class Unbaked( - val fallback: ItemModel.Unbaked, - val overrides: List<Override>, + val fallback: ItemModel.Unbaked, + val overrides: List<Override>, ) : ItemModel.Unbaked { companion object { @JvmStatic @@ -57,10 +57,10 @@ class PredicateModel { val newOverrides = ArrayList<Override>() for (legacyOverride in legacyOverrides) { legacyOverride as JsonObject - val overrideModel = Identifier.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue + val overrideModel = ResourceLocation.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue val predicate = CustomModelOverrideParser.parsePredicates(legacyOverride.getAsJsonObject("predicate")) newOverrides.add(Override( - BasicItemModel.Unbaked(overrideModel, listOf()), + BlockModelWrapper.Unbaked(overrideModel, listOf()), AndPredicate(predicate.toTypedArray()) )) } @@ -69,34 +69,34 @@ class PredicateModel { val OVERRIDE_CODEC: Codec<Override> = RecordCodecBuilder.create { it.group( - ItemModelTypes.CODEC.fieldOf("model").forGetter(Override::model), + ItemModels.CODEC.fieldOf("model").forGetter(Override::model), CustomModelOverrideParser.LEGACY_CODEC.fieldOf("predicate").forGetter(Override::predicate), ).apply(it, Unbaked::Override) } val CODEC: MapCodec<Unbaked> = RecordCodecBuilder.mapCodec { it.group( - ItemModelTypes.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback), + ItemModels.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, + val model: ItemModel.Unbaked, + val predicate: FirmamentModelPredicate, ) - override fun resolve(resolver: ResolvableModel.Resolver) { - fallback.resolve(resolver) - overrides.forEach { it.model.resolve(resolver) } + override fun resolveDependencies(resolver: ResolvableModel.Resolver) { + fallback.resolveDependencies(resolver) + overrides.forEach { it.model.resolveDependencies(resolver) } } - override fun getCodec(): MapCodec<out Unbaked> { + override fun type(): MapCodec<out Unbaked> { return CODEC } - override fun bake(context: ItemModel.BakeContext): ItemModel { + override fun bake(context: ItemModel.BakingContext): 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/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt index 2b13284..52800bd 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.features.texturepack import com.google.gson.JsonArray @@ -13,147 +12,137 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.minecraft.nbt.NbtString -import net.minecraft.text.Text -import moe.nea.firmament.util.MC +import net.minecraft.nbt.StringTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.util.json.intoGson +import moe.nea.firmament.util.json.intoKotlinJson 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.asString() - 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<String> = 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<StringMatcher> { - 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 - } - } + fun matches(string: String): Boolean + fun matches(text: Component): Boolean { + return matches(text.string) + } + + val asRegex: java.util.regex.Pattern + + fun matchWithGroups(string: String): MatchNamedGroupCollection? + + fun matches(nbt: StringTag): 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) { + // TODO: return matches(TextCodecs.CODEC.parse(MC.defaultRegistryNbtOps, string) ?: return false) + } + return matches(string) + } + + class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher { + override val asRegex by lazy(LazyThreadSafetyMode.PUBLICATION) { input.toPattern(java.util.regex.Pattern.LITERAL) } + 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 matchWithGroups(string: String): MatchNamedGroupCollection? { + if (matches(string)) + return object : MatchNamedGroupCollection { + override fun get(name: String): MatchGroup? { + return null + } + + override fun get(index: Int): MatchGroup? { + return null + } + + override val size: Int + get() = 0 + + override fun isEmpty(): Boolean { + return true + } + + override fun contains(element: MatchGroup?): Boolean { + return false + } + + override fun iterator(): Iterator<MatchGroup?> { + return emptyList<MatchGroup>().iterator() + } + + override fun containsAll(elements: Collection<MatchGroup?>): Boolean { + return elements.isEmpty() + } + } + return null + } + + override fun toString(): String { + return "Equals($expected, stripColorCodes = $stripColorCodes)" + } + } + + class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher { + private val pattern = patternWithColorCodes.toRegex() + override val asRegex = pattern.toPattern() + override fun matches(string: String): Boolean { + return pattern.matches(if (stripColorCodes) string.removeColorCodes() else string) + } + + override fun matchWithGroups(string: String): MatchNamedGroupCollection? { + return pattern.matchEntire(if (stripColorCodes) string.removeColorCodes() else string)?.groups as MatchNamedGroupCollection? + } + + override fun toString(): String { + return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)" + } + } + + object Serializer : KSerializer<StringMatcher> { + 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") + } + } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt new file mode 100644 index 0000000..ed486f5 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt @@ -0,0 +1,79 @@ +package moe.nea.firmament.features.texturepack + +import java.util.regex.Matcher +import util.json.CodecSerializer +import kotlinx.serialization.Serializable +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.transformEachRecursively + +@Serializable +data class TreeishTextReplacer( + val match: StringMatcher, + val replacements: List<SubPartReplacement> +) { + @Serializable + data class SubPartReplacement( + val match: StringMatcher, + val style: @Serializable(StyleSerializer::class) Style? = null, + val replace: @Serializable(TextSerializer::class) Component, + ) + + object TextSerializer : CodecSerializer<Component>(ComponentSerialization.CODEC) + object StyleSerializer : CodecSerializer<Style>(Style.Serializer.CODEC) + companion object { + val pattern = "[$]\\{(?<name>[^}]+)}".toPattern() + fun injectMatchResults(text: Component, matches: Matcher): Component { + return text.transformEachRecursively { it -> + val content = it.directLiteralStringContent ?: return@transformEachRecursively it + val matcher = pattern.matcher(content) + val builder = StringBuilder() + while (matcher.find()) { + matcher.appendReplacement(builder, matches.group(matcher.group("name")).toString()) + } + matcher.appendTail(builder) + Component.literal(builder.toString()).setStyle(it.style) + } + } + } + + fun match(text: Component): Boolean { + return match.matches(text) + } + + fun replaceText(text: Component): Component { + return text.transformEachRecursively { part -> + var part: Component = part + for (replacement in replacements) { + val rawPartText = part.string + replacement.style?.let { expectedStyle -> + val parentStyle = part.style + val parented = expectedStyle.applyTo(parentStyle) + if (parented.isStrikethrough != parentStyle.isStrikethrough + || parented.isObfuscated != parentStyle.isObfuscated + || parented.isBold != parentStyle.isBold + || parented.isUnderlined != parentStyle.isUnderlined + || parented.isItalic != parentStyle.isItalic + || parented.color?.value != parentStyle.color?.value) + continue + } + val matcher = replacement.match.asRegex.matcher(rawPartText) + if (!matcher.find()) continue + val p = Component.literal("") + p.setStyle(part.style) + var lastAppendPosition = 0 + do { + p.append(rawPartText.substring(lastAppendPosition, matcher.start())) + lastAppendPosition = matcher.end() + p.append(injectMatchResults(replacement.replace, matcher)) + } while (matcher.find()) + p.append(rawPartText.substring(lastAppendPosition)) + part = p + } + part + } + } + +} 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 index 7e0ddb1..f32b673 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt @@ -4,7 +4,7 @@ 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 +import net.minecraft.world.item.ItemStack object AlwaysPredicate : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { 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 index 99abaaa..c2ed24f 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt @@ -3,15 +3,16 @@ 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.world.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 +import net.minecraft.world.item.ItemStack class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { - override fun test(stack: ItemStack): Boolean { - return children.all { it.test(stack) } - } + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return children.all { it.test(stack, holder) } + } object Parser : FirmamentModelPredicateParser { override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { 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 index 7ccaadf..b54dd5d 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt @@ -1,10 +1,11 @@ package moe.nea.firmament.features.texturepack.predicates import com.google.gson.JsonElement -import net.minecraft.item.ItemStack +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import moe.nea.firmament.util.MC class CastPredicate : FirmamentModelPredicate { object Parser : FirmamentModelPredicateParser { @@ -14,7 +15,11 @@ class CastPredicate : FirmamentModelPredicate { } } + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return (holder as? Player)?.fishing != null && holder.mainHandItem === stack + } + override fun test(stack: ItemStack): Boolean { - return MC.player?.fishHook != null // TODO pass through more of the model predicate context + 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 index 04c7a2b..0d4fb84 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt @@ -5,7 +5,7 @@ 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 net.minecraft.world.item.ItemStack import moe.nea.firmament.util.mc.displayNameAccordingToNbt data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate { 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 index 3c8023d..e94f5c6 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt @@ -1,215 +1,220 @@ package moe.nea.firmament.features.texturepack.predicates -import com.google.gson.JsonArray 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.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 +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.DoubleTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.FloatTag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.LongTag +import net.minecraft.nbt.ShortTag 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 <T : Any> parseGenericNumber( + fun matches(nbt: Tag): 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? IntTag)?.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? FloatTag)?.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? DoubleTag)?.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? LongTag)?.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? ShortTag)?.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? ByteTag)?.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 <T : Any> parseGenericNumber( jsonElement: JsonElement, primitiveExtractor: (JsonPrimitive) -> T?, - crossinline nbtExtractor: (NbtElement) -> T?, + crossinline nbtExtractor: (Tag) -> 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 is NbtString && nbt.asString() == string - } - - override fun toString(): String { - return "MatchNbtStringExactly($string)" - } - } - - class MatchString(val string: StringMatcher) : NbtMatcher { - override fun matches(nbt: NbtElement): Boolean { - return nbt is NbtString && string.matches(nbt.asString()) - } - - override fun toString(): String { - return "MatchNbtString($string)" - } - } + ): 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: Tag): Boolean { + return when (nbt) { + is ByteTag -> nbt.byteValue().toLong() == number + is IntTag -> nbt.intValue().toLong() == number + is ShortTag -> nbt.shortValue().toLong() == number + is LongTag -> nbt.longValue().toLong() == number + else -> false + } + } + + } + + class MatchStringExact(val string: String) : NbtMatcher { + override fun matches(nbt: Tag): Boolean { + return nbt.asString().getOrNull() == string + } + + override fun toString(): String { + return "MatchNbtStringExactly($string)" + } + } + + class MatchString(val string: StringMatcher) : NbtMatcher { + override fun matches(nbt: Tag): Boolean { + return nbt.asString().map(string::matches).getOrDefault(false) + } + + override fun toString(): String { + return "MatchNbtString($string)" + } + } } data class ExtraAttributesPredicate( @@ -217,55 +222,20 @@ data class ExtraAttributesPredicate( 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 pathSegments = if (path is JsonArray) { - path.map { (it as JsonPrimitive).asString } - } else if (path is JsonPrimitive && path.isString) { - path.asString.split(".") - } else return null - val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) - ?: return null - return ExtraAttributesPredicate(NbtPrism(pathSegments), matcher) - } - } - - override fun test(stack: ItemStack): Boolean { - return path.access(stack.extraAttributes) - .any { matcher.matches(it) } - } + 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) } + } } -class NbtPrism(val path: List<String>) { - override fun toString(): String { - return "Prism($path)" - } - fun access(root: NbtElement): Collection<NbtElement> { - var rootSet = mutableListOf(root) - var switch = mutableListOf<NbtElement>() - for (pathSegment in path) { - if (pathSegment == ".") continue - 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/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..4b0b7cb --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt @@ -0,0 +1,59 @@ +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.core.component.DataComponentType +import net.minecraft.world.item.component.CustomData +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.NbtOps +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation +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 +import moe.nea.firmament.util.mc.unsafeNbt + +data class GenericComponentPredicate<T>( + val componentType: DataComponentType<T>, + val codec: Codec<T>, + val path: NbtPrism, + val matcher: NbtMatcher, +) : FirmamentModelPredicate { + constructor(componentType: DataComponentType<T>, 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 CustomData) component.unsafeNbt + 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 + .lookupOrThrow(Registries.DATA_COMPONENT_TYPE) + .getOrThrow( + ResourceKey.create( + Registries.DATA_COMPONENT_TYPE, + ResourceLocation.parse(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 index 3cb80c7..e90d9fb 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt @@ -6,27 +6,27 @@ 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 net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.MC class ItemPredicate( val item: Item ) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { - return stack.item == item + return stack.`is`(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) + val itemKey = ResourceKey.create(Registries.ITEM, + ResourceLocation.tryParse(jsonElement.asString) ?: return null) - return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null) + return ItemPredicate(MC.defaultItems.get(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 index f0b4737..6b2a8b9 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt @@ -5,7 +5,7 @@ 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 net.minecraft.world.item.ItemStack import moe.nea.firmament.util.mc.loreAccordingToNbt class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate { 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 index 4986ad9..d0ad11c 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt @@ -6,7 +6,7 @@ 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 +import net.minecraft.world.item.ItemStack class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { 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 index e3093cd..c32543f 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt @@ -7,7 +7,7 @@ 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 +import net.minecraft.world.item.ItemStack class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { 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 index b30b7c9..9de51d2 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt @@ -7,7 +7,7 @@ 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 net.minecraft.world.item.ItemStack import moe.nea.firmament.repo.ExpLadders import moe.nea.firmament.util.petData 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..330fe88 --- /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.world.entity.LivingEntity +import net.minecraft.world.item.BowItem +import net.minecraft.world.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.getPowerForTime(holder.ticksUsingItem) >= 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..cbef02e --- /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.core.component.DataComponents +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.world.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.`is`(Items.PLAYER_HEAD)) return false + val profile = stack.get(DataComponents.PROFILE) ?: return false + val textureProperty = profile.partialProfile().properties["textures"].firstOrNull() + val textureMode = lazy(LazyThreadSafetyMode.NONE) { + decodeProfileTextureProperty(textureProperty ?: return@lazy null) + } + when { + profileId != null + && profileId != profile.partialProfile().id -> + 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 index 4665829..b998251 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java @@ -1,11 +1,11 @@ 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 net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Display; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; public class ApplyHeadModelInItemRenderer<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { // 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", + @Inject(method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/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..2e22eaa --- /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.resources.model.ModelBaker; +import net.minecraft.client.resources.model.ModelBakery; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ModelBakery.class) +public class BuildExtraBlockStateModels { + @ModifyReturnValue(method = "bakeModels", at = @At("RETURN")) + private CompletableFuture<ModelBakery.BakingResult> injectMoreBlockModels(CompletableFuture<ModelBakery.BakingResult> original, @Local ModelBakery.ModelBakerImpl baker, @Local(argsOnly = true) Executor executor) { + ModelBaker 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 index fede766..3a6e83a 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java @@ -3,24 +3,25 @@ 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 net.minecraft.world.level.block.SkullBlock; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.SkullBlockRenderer; +import net.minecraft.world.item.component.ResolvableProfile; +import net.minecraft.resources.ResourceLocation; 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) +@Mixin(SkullBlockRenderer.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;", + method = "resolveSkullRenderType", at = @At("HEAD"), cancellable = true ) - private static void onGetRenderLayer(SkullBlock.SkullType type, ProfileComponent profile, Identifier texture, CallbackInfoReturnable<RenderLayer> cir) { - CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir); + private void onGetRenderLayer(SkullBlock.Type skullType, SkullBlockEntity blockEntity, CallbackInfoReturnable<RenderType> cir) { + CustomSkyBlockTextures.INSTANCE.modifySkullTexture(skullType, blockEntity.getOwnerProfile(), 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..0ea2c46 --- /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.resources.model.ClientItemInfoLoader; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.ModelDiscovery; +import net.minecraft.client.resources.model.UnbakedModel; +import net.minecraft.resources.ResourceLocation; +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(ModelManager.class) +public class InsertExtraBlockModelDependencies { + @Inject(method = "discoverModelDependencies", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelDiscovery;addSpecialModel(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/UnbakedModel;)V", shift = At.Shift.AFTER)) + private static void insertExtraModels( + Map<ResourceLocation, UnbakedModel> modelMap, + BlockStateModelLoader.LoadedModels stateDefinition, + ClientItemInfoLoader.LoadedClientInfos result, + CallbackInfoReturnable cir, @Local ModelDiscovery 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..2bf89ad --- /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.renderer.item.ItemStackRenderState; +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(ItemStackRenderState.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..5c5cf3e --- /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.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.resources.ResourceLocation; +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(BlockStateModelLoader.class) +public class LoadExtraBlockStates { + @ModifyExpressionValue(method = "loadBlockStates", 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<Map<ResourceLocation, List<Resource>>> loadExtraModels( + CompletableFuture<Map<ResourceLocation, List<Resource>>> x, + @Local(argsOnly = true) Executor executor, + @Local Function<ResourceLocation, StateDefinition<Block, BlockState>> 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 index 669da63..392ef54 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java @@ -6,25 +6,25 @@ 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 net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.world.item.equipment.Equippable; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(ArmorFeatureRenderer.class) +@Mixin(HumanoidArmorLayer.class) public class PatchArmorTexture { @ModifyExpressionValue( - method = "renderArmor", + method = "renderArmorPiece", at = @At( value = "INVOKE", - target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + target = "Lnet/minecraft/world/item/ItemStack;get(Lnet/minecraft/core/component/DataComponentType;)Ljava/lang/Object;")) private Object overrideLayers( - Object original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) EquipmentSlot slot + Object original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) EquipmentSlot slot ) { var overrides = CustomGlobalArmorOverrides.overrideArmor(itemStack, slot); - return overrides.orElse((EquippableComponent) original); + return overrides.orElse((Equippable) 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 index 81ea6cd..5906892 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java @@ -1,23 +1,22 @@ package moe.nea.firmament.mixins.custommodels; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 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.client.render.entity.equipment.EquipmentRenderer; -import net.minecraft.item.equipment.EquipmentAsset; -import net.minecraft.registry.RegistryKey; +import net.minecraft.client.resources.model.EquipmentClientInfo; +import net.minecraft.client.resources.model.EquipmentAssetManager; +import net.minecraft.world.item.equipment.EquipmentAsset; +import net.minecraft.resources.ResourceKey; 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(EquipmentRenderer.class) +@Mixin(EquipmentAssetManager.class) public class PatchLegacyArmorLayerSupport { - @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/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/registry/RegistryKey;)Lnet/minecraft/client/render/entity/equipment/EquipmentModel;")) - private EquipmentModel patchModelLayers(EquipmentModelLoader instance, RegistryKey<EquipmentAsset> assetKey, Operation<EquipmentModel> original) { - var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue()); - if (modelOverride != null) return modelOverride; - return original.call(instance, assetKey); + @Inject(method = "get", at = @At(value = "HEAD"), cancellable = true) + private void patchModelLayers(ResourceKey<EquipmentAsset> assetKey, CallbackInfoReturnable<EquipmentClientInfo> cir) { + var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.location()); + 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..f15a17c --- /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.resources.model.EquipmentClientInfo; +import net.minecraft.resources.ResourceLocation; +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(EquipmentClientInfo.Layer.class) +public class PatchLegacyTexturePathsIntoArmorLayers { + @Shadow + @Final + private ResourceLocation textureId; + + @Inject(method = "getTextureLocation", at = @At("HEAD"), cancellable = true) + private void replaceWith1201TextureIfExists(EquipmentClientInfo.LayerType layerType, CallbackInfoReturnable<ResourceLocation> 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 == EquipmentClientInfo.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 index 9401889..39e23af 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java @@ -4,15 +4,15 @@ 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 net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.core.BlockPos; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(WorldRenderer.class) +@Mixin(LevelRenderer.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;")) diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java index f9a1d0d..7718735 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java @@ -4,26 +4,27 @@ 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 net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(ClientPlayerInteractionManager.class) +@Mixin(MultiPlayerGameMode.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<PositionedSoundInstance> original, - @Local BlockState blockState) { + @WrapOperation(method = "continueDestroyBlock", + at = @At(value = "NEW", target = "(Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FFLnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/client/resources/sounds/SimpleSoundInstance;")) + private SimpleSoundInstance replaceSound( + SoundEvent sound, SoundSource category, float volume, float pitch, + RandomSource random, BlockPos pos, Operation<SimpleSoundInstance> original, + @Local BlockState blockState) { var replacement = CustomBlockTextures.getReplacement(blockState, pos); if (replacement != null && replacement.getSound() != null) { - sound = SoundEvent.of(replacement.getSound()); + sound = SoundEvent.createVariableRangeEvent(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 index 711b2af..c5f40b0 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java @@ -4,35 +4,34 @@ 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.BlockModels; -import net.minecraft.client.render.block.BlockRenderManager; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.util.math.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.chunk.SectionCompiler; +import net.minecraft.client.renderer.block.model.BlockStateModel; +import net.minecraft.core.BlockPos; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Mixin(BlockRenderManager.class) +@Mixin(SectionCompiler.class) public class ReplaceBlockRenderManagerBlockModel { - @WrapOperation(method = "renderBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) - private BakedModel replaceModelInRenderBlock( - BlockRenderManager instance, BlockState state, Operation<BakedModel> 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; - } - - @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<BakedModel> 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; - } + @WrapOperation(method = "compile", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/block/BlockRenderDispatcher;getBlockModel(Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/client/renderer/block/model/BlockStateModel;")) + private BlockStateModel replaceModelInRenderBlock(BlockRenderDispatcher instance, BlockState state, Operation<BlockStateModel> 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<BakedModel> 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 index 53ab74a..f2c2cb8 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java @@ -1,19 +1,19 @@ 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.BakedModel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.renderer.block.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) +@Mixin(BlockModelShaper.class) public class ReplaceFallbackBlockModel { // TODO: add check to BlockDustParticle - @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) - private void getModel(BlockState state, CallbackInfoReturnable<BakedModel> cir) { + @Inject(method = "getBlockModel", at = @At("HEAD"), cancellable = true) + private void getModel(BlockState state, CallbackInfoReturnable<BlockStateModel> 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..1b9bc81 --- /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.renderer.item.ItemModelResolver; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.item.ItemStackRenderState; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.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<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + @Shadow + @Final + protected ItemModelResolver itemModelResolver; + + @Unique + private ItemStackRenderState tempRenderState = new ItemStackRenderState(); + + @Inject( + method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL") + ) + private void replaceHeadModel( + T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci + ) { + var headItemStack = livingEntity.getItemBySlot(EquipmentSlot.HEAD); + + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(true); + tempRenderState.clear(); + this.itemModelResolver.updateForLiving(tempRenderState, headItemStack, ItemDisplayContext.HEAD, livingEntity); + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(false); + + if (HeadModelChooser.HasExplicitHeadModelMarker.cast(tempRenderState) + .isExplicitHeadModel_Firmament()) { + livingEntityRenderState.wornHeadType = null; + var temp = livingEntityRenderState.headItem; + livingEntityRenderState.headItem = 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 index dfc87a0..87d4f13 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java @@ -4,46 +4,40 @@ 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 net.minecraft.client.item.ItemModelManager; -import net.minecraft.client.render.item.model.ItemModel; -import net.minecraft.client.render.item.model.MissingItemModel; -import net.minecraft.client.render.model.BakedModelManager; -import net.minecraft.component.ComponentType; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; +import moe.nea.firmament.util.mc.IntrospectableItemModelManager; +import net.minecraft.client.renderer.item.ItemModelResolver; +import net.minecraft.client.renderer.item.ItemModel; +import net.minecraft.client.renderer.item.MissingItemModel; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceLocation; +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.Unique; 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.function.Function; -@Mixin(ItemModelManager.class) -public class ReplaceItemModelPatch { +@Mixin(ItemModelResolver.class) +public class ReplaceItemModelPatch implements IntrospectableItemModelManager { @Shadow @Final - private Function<Identifier, ItemModel> modelGetter; - - @Inject(method = "<init>", at = @At("TAIL")) - private void saveMissingModel(BakedModelManager bakedModelManager, CallbackInfo ci) { - } - - @Unique - private boolean hasModel(Identifier identifier) { - return !(modelGetter.apply(identifier) instanceof MissingItemModel); - } + private Function<ResourceLocation, ItemModel> modelGetter; @WrapOperation( - method = "update(Lnet/minecraft/client/render/item/ItemRenderState;Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;Lnet/minecraft/world/World;Lnet/minecraft/entity/LivingEntity;I)V", - 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<Object> original) { - var override = CustomItemModelEvent.getModelIdentifier(instance); - if (override != null && hasModel(override)) { + method = "appendItemLayers", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;get(Lnet/minecraft/core/component/DataComponentType;)Ljava/lang/Object;")) + private Object replaceItemModelByIdentifier(ItemStack instance, DataComponentType componentType, Operation<Object> 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 ResourceLocation identifier) { + return !(modelGetter.apply(identifier) instanceof MissingItemModel); + } } diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java deleted file mode 100644 index e4834e9..0000000 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java +++ /dev/null @@ -1,48 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import moe.nea.firmament.features.texturepack.CustomTextColors; -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.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin({HandledScreen.class, InventoryScreen.class, CreativeInventoryScreen.class, MerchantScreen.class, - AnvilScreen.class, BeaconScreen.class}) -public class ReplaceTextColorInHandledScreen { - - // To my future self: double check those mixins, but don't be too concerned about errors. Some of the wrapopertions - // only apply in some of the specified subclasses. - - @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"), - expect = 0, - require = 0) - private int replaceTextColorWithVariableShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) { - return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color), shadow); - } - - @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"), - expect = 0, - require = 0) - private int replaceTextColorWithShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) { - return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color)); - } - -} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextsInDrawContext.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextsInDrawContext.java new file mode 100644 index 0000000..9a568f7 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextsInDrawContext.java @@ -0,0 +1,55 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.features.texturepack.CustomTextReplacements; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.stream.Stream; + +@Mixin(GuiGraphics.class) +public class ReplaceTextsInDrawContext { + // I HATE THIS SO MUCH WHY CANT I JUST OPERATE ON ORDEREDTEXTS!!! + // JUNE I WILL RIP ALL OF THIS OUT AND MAKE YOU REWRITE EVERYTHING + // TODO: be in a mood to rewrite this + + @ModifyVariable(method = "drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V", at = @At("HEAD"), argsOnly = true) + private Component replaceTextInDrawText(Component text) { + return CustomTextReplacements.replaceText(text); + } + + @ModifyVariable(method = "drawCenteredString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;III)V", at = @At("HEAD"), argsOnly = true) + private Component replaceTextInDrawCenteredTextWithShadow(Component text) { + return CustomTextReplacements.replaceText(text); + } + + @ModifyVariable(method = "drawWordWrap", at = @At("HEAD"), argsOnly = true) + private FormattedText replaceTextInDrawWrappedText(FormattedText stringVisitable) { + return stringVisitable instanceof Component text ? CustomTextReplacements.replaceText(text) : stringVisitable; + } + + @ModifyExpressionValue(method = "setComponentTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Ljava/util/List;IILnet/minecraft/resources/ResourceLocation;)V", at = @At(value = "INVOKE", target = "Ljava/util/List;stream()Ljava/util/stream/Stream;")) + private Stream<Component> replaceTextInDrawTooltipListText(Stream<Component> original) { + return original.map(CustomTextReplacements::replaceText); + } + + @ModifyExpressionValue(method = "setTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Ljava/util/List;Ljava/util/Optional;IILnet/minecraft/resources/ResourceLocation;)V", at = @At(value = "INVOKE", target = "Ljava/util/List;stream()Ljava/util/stream/Stream;")) + private Stream<Component> replaceTextInDrawTooltipListTextWithOptional(Stream<Component> original) { + return original.map(CustomTextReplacements::replaceText); + } + + @ModifyVariable(method = "setTooltipForNextFrame(Lnet/minecraft/network/chat/Component;II)V", at = @At("HEAD"), argsOnly = true) + private Component replaceTextInDrawTooltipSingle(Component text) { + return CustomTextReplacements.replaceText(text); + } + + @ModifyExpressionValue(method = "renderComponentHoverEffect", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/HoverEvent$ShowText;value()Lnet/minecraft/network/chat/Component;")) + private Component replaceShowTextInHover(Component text) { + return CustomTextReplacements.replaceText(text); + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java index 8d3b3f8..0165bea 100644 --- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java @@ -4,23 +4,23 @@ 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 net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.resources.model.ClientItemInfoLoader; +import net.minecraft.client.renderer.item.BlockModelWrapper; +import net.minecraft.client.renderer.item.ItemModel; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.PackResources; +import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; -import java.io.IOException; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,63 +30,68 @@ import java.util.concurrent.Executor; import java.util.stream.Collector; import java.util.stream.Collectors; -@Mixin(ItemAssetsLoader.class) +@Mixin(ClientItemInfoLoader.class) public class SupplyFakeModelPatch { @ModifyReturnValue( - method = "load", + method = "scheduleLoad", at = @At("RETURN") ) - private static CompletableFuture<ItemAssetsLoader.Result> injectFakeGeneratedModels( - CompletableFuture<ItemAssetsLoader.Result> original, + private static CompletableFuture<ClientItemInfoLoader.LoadedClientInfos> injectFakeGeneratedModels( + CompletableFuture<ClientItemInfoLoader.LoadedClientInfos> 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) { - Map<Identifier, ItemAsset> newModels = new HashMap<>(oldModels.contents()); - var resources = resourceManager.findResources( + @Unique + private static ClientItemInfoLoader.LoadedClientInfos supplyExtraModels(ResourceManager resourceManager, ClientItemInfoLoader.LoadedClientInfos oldModels) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) return oldModels; + Map<ResourceLocation, ClientItem> newModels = new HashMap<>(oldModels.contents()); + var resources = resourceManager.listResources( "models/item", - id -> id.getNamespace().equals("firmskyblock") - && id.getPath().endsWith(".json") - && !id.getPath().substring("models/item/".length()).contains("/")); - for (Map.Entry<Identifier, Resource> model : resources.entrySet()) { + id -> (id.getNamespace().equals("firmskyblock") || id.getNamespace().equals("cittofirmgenerated")) + && id.getPath().endsWith(".json")); + for (Map.Entry<ResourceLocation, Resource> 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 genericModelId = itemModelId.withPrefix("item/"); + var itemAssetId = itemModelId.withPrefix("items/"); // TODO: inject tint indexes based on the json data here - ItemModel.Unbaked unbakedModel = new BasicItemModel.Unbaked(genericModelId, List.of()); + ItemModel.Unbaked unbakedModel = new BlockModelWrapper.Unbaked(genericModelId, List.of()); // TODO: add a filter using the pack.mcmeta to opt out of this behaviour - try (var is = resource.getInputStream()) { + try (var is = resource.open()) { 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(itemModelId) - .map(Resource::getPack) - .map(it -> isResourcePackNewer(resourceManager, it, resource.getPack())) + if (resourceManager.getResource(itemAssetId.withSuffix(".json")) + .map(Resource::source) + .map(it -> isResourcePackNewer(resourceManager, it, resource.source())) .orElse(true)) { - newModels.put(itemModelId, new ItemAsset( + newModels.put(itemModelId, new ClientItem( unbakedModel, - new ItemAsset.Properties(true) + new ClientItem.Properties(true, true) )); } } - return new ItemAssetsLoader.Result(newModels); + return new ClientItemInfoLoader.LoadedClientInfos(newModels); } + @Unique private static boolean isResourcePackNewer( - ResourceManager manager, - ResourcePack null_, ResourcePack proposal) { - var pack = manager.streamResourcePacks() + ResourceManager manager, + PackResources null_, PackResources proposal) { + var pack = manager.listPacks() .filter(it -> it == null_ || it == proposal) .collect(findLast()); - return pack.orElse(null) == proposal; + return pack.orElse(null_) != null_; } + @Unique private static <T> Collector<T, ?, Optional<T>> 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..3598d37 --- /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.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +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({AbstractContainerScreen.class, AbstractRecipeBookScreen.class}) +public class ExpandScreenBoundaries { + @Inject(method = "hasClickedOutside", at = @At("HEAD"), cancellable = true) + private void onClickOutsideBounds(double mouseX, double mouseY, int left, int top, CallbackInfoReturnable<Boolean> 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/MoveSignElements.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/MoveSignElements.java new file mode 100644 index 0000000..0475a8b --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/MoveSignElements.java @@ -0,0 +1,70 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +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 com.llamalad7.mixinextras.sugar.Share; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(AbstractSignEditScreen.class) +public class MoveSignElements { + @WrapWithCondition( + method = "renderSign", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractSignEditScreen;renderSignBackground(Lnet/minecraft/client/gui/GuiGraphics;)V")) + private boolean onDrawBackgroundSign(AbstractSignEditScreen instance, GuiGraphics drawContext) { + final var override = CustomScreenLayouts.getActiveScreenOverride(); + if (override == null || override.getBackground() == null) return true; + override.getBackground().renderDirect(drawContext); + return false; + } + + @WrapOperation(method = "renderSignText", at = { + @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;textHighlight(IIII)V")} + ) + private void onRenderSignTextSelection( + GuiGraphics instance, int x1, int y1, int x2, int y2, Operation<Void> original, + @Local(index = 9) int messageIndex) { + instance.pose().pushMatrix(); + final var override = CustomScreenLayouts.getSignTextMover(messageIndex); + if (override != null) { + instance.pose().translate(override.getX(), override.getY()); + } + original.call(instance, x1, y1, x2, y2); + instance.pose().popMatrix(); + } + @WrapOperation(method = "renderSignText", at = { + @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;fill(IIIII)V")} + ) + private void onRenderSignTextFill( + GuiGraphics instance, int x1, int y1, int x2, int y2, int color, Operation<Void> original, @Local(index = 9) int messageIndex) { + instance.pose().pushMatrix(); + final var override = CustomScreenLayouts.getSignTextMover(messageIndex); + if (override != null) { + instance.pose().translate(override.getX(), override.getY()); + } + original.call(instance, x1, y1, x2, y2, color); + instance.pose().popMatrix(); + } + + @WrapOperation(method = "renderSignText", at = { + @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Ljava/lang/String;IIIZ)V")}, + expect = 2) + private void onRenderSignTextRendering(GuiGraphics instance, Font textRenderer, String text, int x, int y, int color, boolean shadow, Operation<Void> original, @Local(index = 9) int messageIndex) { + instance.pose().pushMatrix(); + final var override = CustomScreenLayouts.getSignTextMover(messageIndex); + if (override != null) { + instance.pose().translate(override.getX(), override.getY()); + } + original.call(instance, textRenderer, text, x, y, color, shadow); + instance.pose().popMatrix(); + } + +} 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..691c692 --- /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.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AnvilScreen; +import net.minecraft.client.gui.screens.inventory.ItemCombinerScreen; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AnvilMenu; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +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 ItemCombinerScreen<AnvilMenu> { + @Shadow + private EditBox name; + + public ReplaceAnvilScreen(AnvilMenu handler, Inventory playerInventory, Component title, ResourceLocation texture) { + super(handler, playerInventory, title, texture); + } + + @Inject(method = "subInit", at = @At("TAIL")) + private void moveNameField(CallbackInfo ci) { + var override = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getNameField); + if (override == null) return; + int baseX = (this.width - this.imageWidth) / 2; + int baseY = (this.height - this.imageHeight) / 2; + name.setX(baseX + override.getX()); + name.setY(baseY + override.getY()); + if (override.getWidth() != null) + name.setWidth(override.getWidth()); + if (override.getHeight() != null) + name.setHeight(override.getHeight()); + } + + @WrapOperation(method = "renderLabels", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;III)V"), + allow = 1) + private void onDrawRepairCost(GuiGraphics instance, Font textRenderer, Component text, int x, int y, int color, Operation<Void> original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getRepairCostTitle); + 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..8b07fec --- /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.screens.inventory.ItemCombinerScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Inject; + +@Mixin(ItemCombinerScreen.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..d02085a --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java @@ -0,0 +1,32 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractFurnaceScreen; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractFurnaceMenu; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +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<T extends AbstractFurnaceMenu> extends AbstractRecipeBookScreen<T> { + public ReplaceFurnaceBackgrounds(T handler, RecipeBookComponent<?> recipeBook, Inventory inventory, Component title) { + super(handler, recipeBook, inventory, title); + } + + @WrapWithCondition(method = "renderBg", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/ResourceLocation;IIFFIIII)V"), allow = 1) + private boolean onDrawBackground(GuiGraphics instance, RenderPipeline pipeline, ResourceLocation 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..ec6cfc6 --- /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.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.*; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.network.chat.Component; +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, DispenserScreen.class, ContainerScreen.class, HopperScreen.class, ShulkerBoxScreen.class,}) +public abstract class ReplaceGenericBackgrounds extends AbstractContainerScreen<AbstractContainerMenu> { + // TODO: split out screens with special background components like flames, arrows, etc. (maybe arrows deserve generic handling tho) + public ReplaceGenericBackgrounds(AbstractContainerMenu handler, Inventory inventory, Component title) { + super(handler, inventory, title); + } + + @Inject(method = "renderBg", at = @At("HEAD"), cancellable = true) + private void replaceDrawBackground(GuiGraphics 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..d089d6d --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java @@ -0,0 +1,51 @@ +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 com.mojang.blaze3d.pipeline.RenderPipeline; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +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 AbstractRecipeBookScreen<InventoryMenu> { + public ReplacePlayerBackgrounds(InventoryMenu handler, RecipeBookComponent<?> recipeBook, Inventory inventory, Component title) { + super(handler, recipeBook, inventory, title); + } + + + @WrapOperation(method = "renderLabels", + allow = 1, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V")) + private void onDrawForegroundText(GuiGraphics instance, Font textRenderer, Component text, int x, int y, int color, boolean shadow, Operation<Void> original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle); + original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color), + shadow); + } + + @WrapWithCondition(method = "renderBg", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/ResourceLocation;IIFFIIII)V")) + private boolean onDrawBackground(GuiGraphics instance, RenderPipeline pipeline, ResourceLocation 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..6dbbbbe --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java @@ -0,0 +1,60 @@ +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.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +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(AbstractContainerScreen.class) +// TODO: MerchantScreen.class, BeaconScreen.class +public class ReplaceTextColorInHandledScreen { + + @WrapOperation( + method = "renderLabels", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V"), + slice = @Slice( + from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;title:Lnet/minecraft/network/chat/Component;", opcode = Opcodes.GETFIELD), + to = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;playerInventoryTitle:Lnet/minecraft/network/chat/Component;", opcode = Opcodes.GETFIELD) + ), + allow = 1, + require = 1) + private void replaceContainerTitle(GuiGraphics instance, Font textRenderer, Component text, int x, int y, int color, boolean shadow, Operation<Void> original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle); + original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color), + shadow); + } + + @WrapOperation( + method = "renderLabels", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V"), + slice = @Slice( + from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;playerInventoryTitle:Lnet/minecraft/network/chat/Component;", opcode = Opcodes.GETFIELD), + to = @At(value = "TAIL") + ), + allow = 1, + require = 1) + private void replacePlayerTitle(GuiGraphics instance, Font textRenderer, Component text, int x, int y, int color, boolean shadow, Operation<Void> original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getPlayerTitle); + 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 index 2a7bcc8..2084af4 100644 --- a/symbols/build.gradle.kts +++ b/symbols/build.gradle.kts @@ -1,12 +1,9 @@ - plugins { kotlin("jvm") id("com.google.devtools.ksp") + id("firmament.common") } -repositories { - mavenCentral() -} dependencies { ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") implementation("com.google.auto.service:auto-service-annotations:1.1.1") diff --git a/symbols/src/main/kotlin/process/CompatMetaProcessor.kt b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt new file mode 100644 index 0000000..6f32b85 --- /dev/null +++ b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt @@ -0,0 +1,64 @@ +package moe.nea.firmament.annotations.process + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.getClassDeclarationByName +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<KSAnnotated> { + 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/ConfigAnnotationProcessor.kt b/symbols/src/main/kotlin/process/ConfigAnnotationProcessor.kt new file mode 100644 index 0000000..028f386 --- /dev/null +++ b/symbols/src/main/kotlin/process/ConfigAnnotationProcessor.kt @@ -0,0 +1,73 @@ +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.KSFile +import com.google.devtools.ksp.symbol.KSName + +class ConfigAnnotationProcessor( + val logger: KSPLogger, val codeGenerator: CodeGenerator, val sourceSetName: String +) : SymbolProcessor { + val configs = mutableSetOf<Pair<KSName, KSFile>>() + override fun process(resolver: Resolver): List<KSAnnotated> { + resolver.getSymbolsWithAnnotation("moe.nea.firmament.util.data.Config") + .map { it as KSClassDeclaration } + .mapTo(configs) { + it.qualifiedName!! to it.containingFile!! + } + return listOf() + } + + override fun finish() { + val dependencies = Dependencies( + aggregating = true, + *configs.map { it.second }.toTypedArray() + ) + val generatedFileName = "ConfigProvider" + val compatFile = + codeGenerator.createNewFile( + dependencies, + "moe.nea.firmament.annotations.generated.$sourceSetName", + generatedFileName + ) + .bufferedWriter() + compatFile.appendLine("// This file is @generated by ConfigAnnotationProcessor") + compatFile.appendLine("// Do not edit") + compatFile.appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") + compatFile.appendLine("class $generatedFileName : moe.nea.firmament.util.data.IConfigProvider {") + compatFile.appendLine( + """ + override val configs: List<moe.nea.firmament.util.data.IDataHolder<*>> = listOf(${configs.joinToString { it.first.asString() }}) + """ + ) + compatFile.appendLine("}") + compatFile.close() + val metaInf = codeGenerator.createNewFileByPath( + dependencies, + "META-INF/services/moe.nea.firmament.util.data.IConfigProvider", extensionName = "" + ) + .bufferedWriter() + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") + metaInf.appendLine(generatedFileName) + metaInf.close() + } + + @AutoService(SymbolProcessorProvider::class) + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return ConfigAnnotationProcessor( + environment.logger, + environment.codeGenerator, + environment.options["firmament.sourceset"] ?: "main" + ) + } + } +} diff --git a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt index d7aaf28..3eaf3d6 100644 --- a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt +++ b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt @@ -25,22 +25,19 @@ class SubscribeAnnotationProcessor( override fun finish() { subscriptions.sort() if (subscriptions.isEmpty()) return - val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.parent.containingFile!! } + 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", generatedFileName) + .createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName) .bufferedWriter() subscriptionsFile.apply { appendLine("// This file is @generated by SubscribeAnnotationProcessor") appendLine("// Do not edit") - for (file in subscriptionSet) { - appendLine("// Dependency: ${file.filePath}") - } - appendLine("package moe.nea.firmament.annotations.generated") + appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") appendLine() appendLine("import moe.nea.firmament.events.subscription.*") appendLine() @@ -48,7 +45,7 @@ class SubscribeAnnotationProcessor( appendLine("class $generatedFileName : SubscriptionList {") appendLine(" override fun provideSubscriptions(addSubscription: (Subscription<*>) -> Unit) {") for (subscription in subscriptions) { - val owner = subscription.parent.qualifiedName!!.asString() + val owner = subscription.pQName.asString() val method = subscription.child.simpleName.asString() val type = subscription.type.declaration.qualifiedName!!.asString() appendLine(" addSubscription(Subscription<$type>(") @@ -65,7 +62,7 @@ class SubscribeAnnotationProcessor( dependencies, "META-INF/services/moe.nea.firmament.events.subscription.SubscriptionList", extensionName = "") .bufferedWriter() - metaInf.append("moe.nea.firmament.annotations.generated.") + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") metaInf.appendLine(generatedFileName) metaInf.close() } @@ -75,13 +72,15 @@ class SubscribeAnnotationProcessor( val child: KSFunctionDeclaration, val type: KSType, ) : Comparable<Subscription> { + val cf = parent.containingFile!! + val pQName = parent.qualifiedName!! + val tName = type.declaration.qualifiedName!! override fun compareTo(other: Subscription): Int { - var compare = parent.qualifiedName!!.asString().compareTo(other.parent.qualifiedName!!.asString()) + 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.type.declaration.qualifiedName!!.asString() - .compareTo(type.declaration.qualifiedName!!.asString()) + compare = other.tName.asString().compareTo(tName.asString()) if (compare != 0) return compare return 0 } diff --git a/testagent/build.gradle.kts b/testagent/build.gradle.kts index 3bd8c8c..cf7c15b 100644 --- a/testagent/build.gradle.kts +++ b/testagent/build.gradle.kts @@ -1,6 +1,7 @@ plugins { java - alias(libs.plugins.shadow) + id("firmament.common") + id("com.gradleup.shadow") } dependencies { implementation(libs.asm) diff --git a/translations/en_us.json b/translations/en_us.json index 82dcd45..a11d4d6 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -7,7 +7,8 @@ "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.import.error": "Could not import waypoints from clipboard. Make sure they are on ColeWeight format:\n[{\"x\": 69, \"y\":420, \"z\": 36}]", + "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", @@ -23,6 +24,21 @@ "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.", @@ -34,16 +50,23 @@ "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.", @@ -63,11 +86,19 @@ "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.", @@ -79,15 +110,20 @@ "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", @@ -97,6 +133,25 @@ "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-fail": "Fail Colour", + "firmament.config.etherwarp-overlay.cube-colour-fail.description": "Cube Colour on Fail (Blocks above occupied)", + "firmament.config.etherwarp-overlay.cube-colour-tooclose": "Too Close Colour", + "firmament.config.etherwarp-overlay.cube-colour-tooclose.description": "Cube Colour if the warp would succeed, but you will interact with the block instead (you are too close, but no blocks above)", + "firmament.config.etherwarp-overlay.cube-colour-toofar": "Too Far Colour", + "firmament.config.etherwarp-overlay.cube-colour-toofar.description": "Cube Colour if the warp would succeed, if you were closer (too far away, but no blocks above)", + "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.failure-text": "Show Failure Text", + "firmament.config.etherwarp-overlay.failure-text.description": "Show a text in game explaining why the teleport fails.", + "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.", @@ -108,19 +163,55 @@ "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-toggle": "Show Sprint State Hud", + "firmament.config.fixes.auto-sprint-hud-toggle.description": "Whether or not to show the 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 off-hand 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-scroll": "Scroll Peeked Chat", + "firmament.config.fixes.peek-chat-scroll.description": "Allow scrolling chat while peeking.", "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.inventory-buttons": "Inventory buttons", - "firmament.config.inventory-buttons.open-editor": "Open Editor", - "firmament.config.inventory-buttons.open-editor.description": "Click anywhere to create a new inventory button or to edit one. Hold SHIFT to grid align.", + "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.", @@ -129,14 +220,28 @@ "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.lore-timers": "Lore Timers", + "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", @@ -148,8 +253,23 @@ "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-style": "Pet Info Hud Colour", + "firmament.config.pets.pet-overlay-hud-style.choice.colour_background": "Colour With Background", + "firmament.config.pets.pet-overlay-hud-style.choice.colour_no_background": "Colour No Background", + "firmament.config.pets.pet-overlay-hud-style.choice.icon_only": "Icon Only", + "firmament.config.pets.pet-overlay-hud-style.choice.plain_background": "Plain With Background", + "firmament.config.pets.pet-overlay-hud-style.choice.plain_no_background": "Plain No Background", + "firmament.config.pets.pet-overlay-hud-style.description": "Determines the style of the Pet Info Hud.", + "firmament.config.pets.pet-overlay-hud.description": "A HUD showing information about your currently active pet. Make sure the pet widget is enabled and visible in /tablist for full accuracy.", + "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-colour": "Ability Cooldown Colour", + "firmament.config.pickaxe-info.ability-colour.description": "Color of the cooldown indicator around the cross-hair.", "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.", @@ -158,9 +278,18 @@ "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.pickaxe-info.show-on-tools": "Show on Tools", + "firmament.config.pickaxe-info.show-on-tools.choice.all": "All Tools", + "firmament.config.pickaxe-info.show-on-tools.choice.drills": "Drills", + "firmament.config.pickaxe-info.show-on-tools.choice.pickaxes_and_drills": "Drills & Pickaxes", + "firmament.config.pickaxe-info.show-on-tools.description": "Show pickaxe ability cooldowns only on some tools. Notabene: Not all tools have support for pickaxe abilities so you might need to swap to your drill to activate the abilities.", "firmament.config.power-user": "Power Users", + "firmament.config.power-user.copy-animated-skins": "Copy animated skins", + "firmament.config.power-user.copy-animated-skins.description": "Copy animated skin previews when spawned from Elizabeth.", "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", @@ -169,19 +298,41 @@ "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-skull-texture": "Copy Skull Texture", + "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. This will work for placed skulls, as well as those in your inventory.", "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.power-user.slot-numbers": "Show slot numbers", + "firmament.config.power-user.slot-numbers.description": "Hold to show slot indices.", + "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.", @@ -202,8 +353,15 @@ "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", @@ -225,34 +383,87 @@ "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-only-in-inv": "Bind Only in Inventory", + "firmament.config.slot-locking.bind-only-in-inv.description": "Only allow using slot binds in the player inventory, not in chests, etc.", "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.inactive-page-tooltips": "Inactive Page Tooltips", + "firmament.config.storage-overlay.inactive-page-tooltips.description": "Show item tooltips when hovering over items on pages other than the active one.", "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.allow-unequipping": "Allow unequipping", + "firmament.config.wardrobe-keybinds.allow-unequipping.description": "Allow unequipping items via wardrobe hotkeys.", + "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", @@ -275,12 +486,16 @@ "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.mixins.start": "Applied firmament mixins:", "firmament.modapi.event": "Received mod API event: %s", "firmament.poweruser.entity.armor": "Entity Armor:", "firmament.poweruser.entity.armor.item": " - %s", @@ -299,7 +514,6 @@ "firmament.price.lowestbin": "Lowest BIN: %s", "firmament.pristine-profit.collection": "Collection: %s/h", "firmament.pristine-profit.money": "Money: %s/h", - "firmament.protectitem": "Firmament protected your item: ", "firmament.pv.lookingup": "Looking up %s", "firmament.pv.noplayer": "%s is not a Minecraft player", "firmament.pv.noprofile": "%s has no SkyBlock profiles", @@ -331,8 +545,6 @@ "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.recipecategory.reforge": "Reforge", - "firmament.recipecategory.reforge.basic": "This is a basic reforge, available at the Blacksmith.", "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.", @@ -348,14 +560,15 @@ "firmament.sbinfo.server": "Locraw Server: %s", "firmament.toggle.false": "Off", "firmament.toggle.true": "On", - "firmament.tooltip.ah.lowestbin": "Lowest BIN: %d", - "firmament.tooltip.bazaar.buy-order": "Bazaar Buy Order: %s", - "firmament.tooltip.bazaar.sell-order": "Bazaar Sell Order: %s", "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", @@ -369,5 +582,10 @@ "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", - "firmanent.config.edit": "Edit" + "key.category.firmament.dev": "Firmament > Dev", + "key.category.firmament.events": "Firmament > Events", + "key.category.firmament.inventory": "Firmament > Inventory", + "key.category.firmament.items": "Firmament > Items", + "key.category.firmament.misc": "Firmament > Misc", + "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/languages/zh_cn.json b/translations/languages/zh_cn.json new file mode 100644 index 0000000..3612ea4 --- /dev/null +++ b/translations/languages/zh_cn.json @@ -0,0 +1,535 @@ +{ + "config.jade.plugin_firmament.custom_mining_hardness": "Firmament挖掘硬度", + "config.jade.plugin_firmament.toolprovider": "Firmament工具提供者", + "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.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-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-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.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.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/REUSE.toml b/web/REUSE.toml index 951c8b9..00357d6 100644 --- a/web/REUSE.toml +++ b/web/REUSE.toml @@ -10,7 +10,7 @@ SPDX-License-Identifier = "GPL-3.0-or-later" SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"] [[annotations]] -path = ["pnpm-lock.yaml"] +path = ["pnpm-lock.yaml", "pnpm-workspace.yaml"] SPDX-License-Identifier = "CC0-1.0" SPDX-FileCopyrightText = ["Auto Generated"] diff --git a/web/astro.config.mjs b/web/astro.config.mjs index 6f7f837..a043486 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -3,7 +3,12 @@ import { defineConfig } from 'astro/config'; import tailwind from '@astrojs/tailwind'; +import mdx from "@astrojs/mdx"; + // https://astro.build/config export default defineConfig({ - integrations: [tailwind()] + integrations: [tailwind(), mdx()], + redirects: { + "/discord": "https://discord.com/invite/64pFP94AWA", + }, });
\ No newline at end of file diff --git a/web/package.json b/web/package.json index 0cf9347..f998393 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "packageManager": "pnpm@9.3.0", + "packageManager": "pnpm@10.17.0+sha512.fce8a3dd29a4ed2ec566fb53efbb04d8c44a0f05bc6f24a73046910fb9c3ce7afa35a0980500668fa3573345bd644644fa98338fa168235c80f4aa17aa17fbef", "name": "", "type": "module", "version": "0.0.1", @@ -12,10 +12,11 @@ }, "dependencies": { "@astrojs/check": "^0.9.4", - "@astrojs/tailwind": "^5.1.2", - "astro": "^4.16.13", + "@astrojs/mdx": "^4.3.5", + "@astrojs/tailwind": "^6.0.2", + "astro": "^5.13.8", "sharp": "^0.33.5", - "tailwindcss": "^3.4.15", - "typescript": "^5.6.3" + "tailwindcss": "^3.4.17", + "typescript": "^5.9.2" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index eb1b894..9ead218 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -10,22 +10,25 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.4 - version: 0.9.4(typescript@5.6.3) + version: 0.9.4(typescript@5.9.2) + '@astrojs/mdx': + specifier: ^4.3.5 + version: 4.3.5(astro@5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1)) '@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) + specifier: ^6.0.2 + version: 6.0.2(astro@5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17) astro: - specifier: ^4.16.13 - version: 4.16.13(rollup@4.27.2)(typescript@5.6.3) + specifier: ^5.13.8 + version: 5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1) sharp: specifier: ^0.33.5 version: 0.33.5 tailwindcss: - specifier: ^3.4.15 - version: 3.4.15 + specifier: ^3.4.17 + version: 3.4.17 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.2 + version: 5.9.2 packages: @@ -33,21 +36,17 @@ packages: 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/compiler@2.13.0': + resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==} - '@astrojs/internal-helpers@0.4.1': - resolution: {integrity: sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==} + '@astrojs/internal-helpers@0.7.2': + resolution: {integrity: sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==} '@astrojs/language-server@2.15.4': resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} @@ -61,108 +60,51 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-remark@5.3.0': - resolution: {integrity: sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==} + '@astrojs/markdown-remark@6.3.6': + resolution: {integrity: sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug==} + + '@astrojs/mdx@4.3.5': + resolution: {integrity: sha512-YB3Hhsvl1BxyY0ARe1OrnVzLNKDPXAz9epYvmL+MQ8A85duSsSLQaO3GHB6/qZJKNoLmP6PptOtCONCKkbhPeQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + peerDependencies: + astro: ^5.0.0 - '@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/prism@3.3.0': + resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - '@astrojs/tailwind@5.1.2': - resolution: {integrity: sha512-IvOF0W/dtHElcXvhrPR35nHmhyV3cfz1EzPitMGtU7sYy9Hci3BNK1To6FWmVuuNKPxza1IgCGetSynJZL7fOg==} + '@astrojs/tailwind@6.0.2': + resolution: {integrity: sha512-j3mhLNeugZq6A8dMNXVarUa8K6X9AW+QHU9u3lKNrPLMHhOQ0S7VeWhHwEeJFpEK1BTKEUY1U78VQv2gN6hNGg==} peerDependencies: - astro: ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 + astro: ^3.0.0 || ^4.0.0 || ^5.0.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/telemetry@3.3.0': + resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.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==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} 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==} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.25.9': - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} 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'} + '@capsizecss/unpack@2.4.0': + resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} '@emmetio/abbreviation@2.3.3': resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} @@ -185,273 +127,415 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} cpu: [x64] os: [win32] + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + '@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-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + 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-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + 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-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + 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-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + 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-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + 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-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + 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-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + 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-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + 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-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + 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-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + 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-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + 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-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + 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-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + 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-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + 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-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + 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-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@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-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + 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] + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + 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/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@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.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -472,8 +556,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/pluginutils@5.1.3': - resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -481,131 +565,146 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.27.2': - resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} + '@rollup/rollup-android-arm-eabi@4.50.2': + resolution: {integrity: sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.27.2': - resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} + '@rollup/rollup-android-arm64@4.50.2': + resolution: {integrity: sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.27.2': - resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} + '@rollup/rollup-darwin-arm64@4.50.2': + resolution: {integrity: sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.27.2': - resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} + '@rollup/rollup-darwin-x64@4.50.2': + resolution: {integrity: sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.27.2': - resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} + '@rollup/rollup-freebsd-arm64@4.50.2': + resolution: {integrity: sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.27.2': - resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} + '@rollup/rollup-freebsd-x64@4.50.2': + resolution: {integrity: sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': - resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': + resolution: {integrity: sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.27.2': - resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} + '@rollup/rollup-linux-arm-musleabihf@4.50.2': + resolution: {integrity: sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.27.2': - resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} + '@rollup/rollup-linux-arm64-gnu@4.50.2': + resolution: {integrity: sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.27.2': - resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} + '@rollup/rollup-linux-arm64-musl@4.50.2': + resolution: {integrity: sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': - resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} + '@rollup/rollup-linux-loong64-gnu@4.50.2': + resolution: {integrity: sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.2': + resolution: {integrity: sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.27.2': - resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} + '@rollup/rollup-linux-riscv64-gnu@4.50.2': + resolution: {integrity: sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.27.2': - resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} + '@rollup/rollup-linux-riscv64-musl@4.50.2': + resolution: {integrity: sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.2': + resolution: {integrity: sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.27.2': - resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} + '@rollup/rollup-linux-x64-gnu@4.50.2': + resolution: {integrity: sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.27.2': - resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} + '@rollup/rollup-linux-x64-musl@4.50.2': + resolution: {integrity: sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.27.2': - resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} + '@rollup/rollup-openharmony-arm64@4.50.2': + resolution: {integrity: sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.2': + resolution: {integrity: sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.27.2': - resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} + '@rollup/rollup-win32-ia32-msvc@4.50.2': + resolution: {integrity: sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.27.2': - resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} + '@rollup/rollup-win32-x64-msvc@4.50.2': + resolution: {integrity: sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==} cpu: [x64] os: [win32] - '@shikijs/core@1.23.0': - resolution: {integrity: sha512-J4Fo22oBlfRHAXec+1AEzcowv+Qdf4ZQkuP/X/UHYH9+KA9LvyFXSXyS+HxuBRFfon+u7bsmKdRBjoZlbDVRkQ==} + '@shikijs/core@3.12.2': + resolution: {integrity: sha512-L1Safnhra3tX/oJK5kYHaWmLEBJi1irASwewzY3taX5ibyXyMkkSDZlq01qigjryOBwrXSdFgTiZ3ryzSNeu7Q==} - '@shikijs/engine-javascript@1.23.0': - resolution: {integrity: sha512-CcrppseWShG+8Efp1iil9divltuXVdCaU4iu+CKvzTGZO5RmXyAiSx668M7VbX8+s/vt1ZKu75Vn/jWi8O3G/Q==} + '@shikijs/engine-javascript@3.12.2': + resolution: {integrity: sha512-Nm3/azSsaVS7hk6EwtHEnTythjQfwvrO5tKqMlaH9TwG1P+PNaR8M0EAKZ+GaH2DFwvcr4iSfTveyxMIvXEHMw==} - '@shikijs/engine-oniguruma@1.23.0': - resolution: {integrity: sha512-gS8bZLqVvmZXX+E5JUMJICsBp+kx6gj79MH/UEpKHKIqnUzppgbmEn6zLa6mB5D+sHse2gFei3YYJxQe1EzZXQ==} + '@shikijs/engine-oniguruma@3.12.2': + resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} - '@shikijs/types@1.23.0': - resolution: {integrity: sha512-HiwzsihRao+IbPk7FER/EQT/D0dEEK3n5LAtHDzL5iRT+JMblA7y9uitUnjEnHeLkKigNM+ZplrP7MuEyyc5kA==} + '@shikijs/langs@3.12.2': + resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} - '@shikijs/vscode-textmate@9.3.0': - resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + '@shikijs/themes@3.12.2': + resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@shikijs/types@3.12.2': + resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@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==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} '@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/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fontkit@2.0.8': + resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -613,46 +712,60 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/ms@0.7.34': - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@24.5.1': + resolution: {integrity: sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@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==} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@volar/kit@2.4.10': - resolution: {integrity: sha512-ul+rLeO9RlFDgkY/FhPWMnpFqAsjvjkKz8VZeOY5YCJMwTblmmSBlNJtFNxSBx9t/k1q80nEthLyxiJ50ZbIAg==} + '@volar/kit@2.4.23': + resolution: {integrity: sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ==} peerDependencies: typescript: '*' - '@volar/language-core@2.4.10': - resolution: {integrity: sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==} + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} - '@volar/language-server@2.4.10': - resolution: {integrity: sha512-odQsgrJh8hOXfxkSj/BSnpjThb2/KDhbxZnG/XAEx6E3QGDQv4hAOz9GWuKoNs0tkjgwphQGIwDMT1JYaTgRJw==} + '@volar/language-server@2.4.23': + resolution: {integrity: sha512-k0iO+tybMGMMyrNdWOxgFkP0XJTdbH0w+WZlM54RzJU3WZSjHEupwL30klpM7ep4FO6qyQa03h+VcGHD4Q8gEg==} - '@volar/language-service@2.4.10': - resolution: {integrity: sha512-VxUiWS11rnRzakkqw5x1LPhsz+RBfD0CrrFarLGW2/voliYXEdCuSOM3r8JyNRvMvP4uwhD38ccAdTcULQEAIQ==} + '@volar/language-service@2.4.23': + resolution: {integrity: sha512-h5mU9DZ/6u3LCB9xomJtorNG6awBNnk9VuCioGsp6UtFiM8amvS5FcsaC3dabdL9zO0z+Gq9vIEMb/5u9K6jGQ==} - '@volar/source-map@2.4.10': - resolution: {integrity: sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==} + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} - '@volar/typescript@2.4.10': - resolution: {integrity: sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==} + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - '@vscode/emmet-helper@2.10.0': - resolution: {integrity: sha512-UHw1EQRgLbSYkyB73/7wR/IzV6zTBnbzEHuuU4Z6b95HKf2lmeTdGwBIwspWBSRrnIA1TI2x2tetBym6ErA7Gw==} + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -666,16 +779,16 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} 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==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} any-promise@1.3.0: @@ -688,9 +801,6 @@ packages: 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==} @@ -701,13 +811,17 @@ packages: 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'} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + astro@5.13.8: + resolution: {integrity: sha512-SNURCAlfL4Z2ylF3NMmNk/s3RnSDSolXALXtH0gsN8hFZ7oppnF0sXVQLAGAxnzADemfRp3/9G58EALZ36qUdA==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -726,23 +840,36 @@ packages: base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.5: + resolution: {integrity: sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==} + hasBin: true + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + blob-to-buffer@1.2.9: + resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + 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==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} 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==} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -754,14 +881,14 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001680: - resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + caniuse-lite@1.0.30001743: + resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} character-entities-html4@2.1.0: @@ -773,38 +900,40 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + 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==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - ci-info@4.1.0: - resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + ci-info@4.3.0: + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} 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'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -829,24 +958,34 @@ packages: 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-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - cross-spawn@7.0.5: - resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + 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==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -854,27 +993,36 @@ packages: supports-color: optional: true - decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.0: + resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} 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==} + devalue@5.3.2: + resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -892,17 +1040,14 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.62: - resolution: {integrity: sha512-t8c+zLmJHa9dJy96yBZRXGQYoiCEnHYgFwn1asvSPZSUdVxnB62A4RASd7k41ytG3ErFBA0TpHlKg9D9SQBmLg==} + electron-to-chromium@1.5.220: + resolution: {integrity: sha512-TWXijEwR1ggr4BdAKrb1nMNqYLTx1/4aD1fkeZU+FVJGTKu53/T7UyHKXlqEX3Ub02csyHePbHmkvnrjcaYzMA==} 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@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -910,16 +1055,22 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - es-module-lexer@1.5.4: - resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} hasBin: true escalade@3.2.0: @@ -930,10 +1081,23 @@ packages: 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-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -944,47 +1108,47 @@ packages: 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==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-uri@3.0.3: - resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true 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==} + fontace@0.3.0: + resolution: {integrity: sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==} + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} fraction.js@4.3.7: @@ -998,16 +1162,12 @@ packages: 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==} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} github-slugger@2.0.0: @@ -1025,16 +1185,8 @@ packages: 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'} + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} @@ -1043,8 +1195,8 @@ packages: 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-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} @@ -1055,8 +1207,14 @@ packages: 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-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} @@ -1067,8 +1225,8 @@ packages: 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==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -1076,32 +1234,43 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} 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==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + 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'} @@ -1114,15 +1283,14 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + 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'} @@ -1131,14 +1299,6 @@ packages: 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'} @@ -1149,44 +1309,23 @@ packages: 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==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} 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'} @@ -1195,55 +1334,40 @@ packages: 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==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 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==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + 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-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -1251,8 +1375,8 @@ packages: 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-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} mdast-util-gfm-strikethrough@2.0.0: resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} @@ -1263,8 +1387,20 @@ packages: 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-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -1278,12 +1414,15 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + 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-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -1294,8 +1433,8 @@ packages: 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-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} micromark-extension-gfm-tagfilter@2.0.0: resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} @@ -1306,12 +1445,30 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + 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-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + micromark-factory-space@2.0.1: resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} @@ -1342,6 +1499,9 @@ packages: micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} @@ -1354,26 +1514,22 @@ packages: 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-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - micromark-util-types@2.0.1: - resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromark@4.0.1: - resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} 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'} @@ -1382,8 +1538,8 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} ms@2.1.3: @@ -1395,8 +1551,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1407,8 +1563,23 @@ packages: nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -1426,57 +1597,51 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} - oniguruma-to-es@0.1.2: - resolution: {integrity: sha512-sBYKVJlIMB0WPO+tSu/NNB1ytSFeHyyJZ3Ayxfx3f/QUuXu0lvZk0VB4K7npmdlHSC0ldqanzh/sUSlAbgCTfw==} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - ora@8.1.1: - resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} - engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - p-limit@6.1.0: - resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} 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==} + p-queue@8.1.1: + resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} engines: {node: '>=18'} - p-timeout@6.1.3: - resolution: {integrity: sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} 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==} + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 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'} @@ -1495,34 +1660,26 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 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==} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} 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==} + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 @@ -1552,21 +1709,17 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 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==} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} prompts@2.4.2: @@ -1576,9 +1729,15 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -1586,18 +1745,32 @@ packages: 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'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - regex-recursion@4.2.1: - resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@4.4.0: - resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -1605,20 +1778,26 @@ packages: rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + 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-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-rehype@11.1.1: - resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} remark-smartypants@3.0.2: resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} @@ -1641,13 +1820,13 @@ packages: 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==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} hasBin: true - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -1661,28 +1840,20 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.27.2: - resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} + rollup@4.50.2: + resolution: {integrity: sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==} 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==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true @@ -1690,6 +1861,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + 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'} @@ -1698,33 +1873,34 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@1.23.0: - resolution: {integrity: sha512-xfdu9DqPkIpExH29cmiTlgo0/jBki5la1Tkfhsv+Wu5TT3APLNHslR1acxuKJOCWqVdSc+pIbs/2ozjVRGppdg==} + shiki@3.12.2: + resolution: {integrity: sha512-uIrKI+f9IPz1zDT+GMz+0RjzKJiijVr6WDWm9Pe3NNY6QigKCfifCEv9v9R2mDASKKjzjQ2QpFLcxaR3iHSnMA==} 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==} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smol-toml@1.4.2: + resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + 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'} @@ -1744,17 +1920,15 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} @@ -1765,8 +1939,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss@3.4.15: - resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==} + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true @@ -1777,13 +1951,23 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tinyexec@0.3.1: - resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -1793,8 +1977,8 @@ packages: 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==} + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -1806,24 +1990,45 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-fest@4.27.0: - resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} 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-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unifont@0.5.2: + resolution: {integrity: sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg==} + unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -1833,6 +2038,9 @@ packages: unist-util-modify-children@4.0.0: resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} @@ -1851,8 +2059,70 @@ packages: 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==} + unstorage@1.17.1: + resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1863,28 +2133,33 @@ packages: vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} 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} + vite@6.3.6: + resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -1899,11 +2174,15 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true - vitefu@1.0.3: - resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==} + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: vite: optional: true @@ -1967,11 +2246,11 @@ packages: '@volar/language-service': optional: true - vscode-css-languageservice@6.3.1: - resolution: {integrity: sha512-1BzTBuJfwMc3A0uX4JBdJgoxp74cjj4q2mDJdp49yD/GuAq4X0k5WtK6fNcMYr+FfJ9nqgR6lpfCSZDkARJ5qQ==} + vscode-css-languageservice@6.3.7: + resolution: {integrity: sha512-5TmXHKllPzfkPhW4UE9sODV3E0bIOJPOk+EERKllf2SmAczjfTmYeq5txco+N3jpF8KIZ6loj/JptpHBQuVQRA==} - vscode-html-languageservice@5.3.1: - resolution: {integrity: sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==} + vscode-html-languageservice@5.5.1: + resolution: {integrity: sha512-/ZdEtsZ3OiFSyL00kmmu7crFV9KwWR+MgpzjsxO60DQH7sIfHZM892C/E4iDd11EKocr+NYuvOA4Y7uc3QzLEA==} vscode-json-languageservice@4.1.8: resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} @@ -2011,20 +2290,22 @@ packages: vscode-nls@5.2.0: resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + 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'} @@ -2042,20 +2323,17 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - xxhash-wasm@1.0.2: - resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} 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 @@ -2064,9 +2342,9 @@ packages: 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'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@21.1.1: @@ -2077,14 +2355,22 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yocto-queue@1.1.1: - resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zod-to-json-schema@3.23.5: - resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: - zod: ^3.23.3 + zod: ^3.24.1 zod-to-ts@1.2.0: resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} @@ -2092,8 +2378,8 @@ packages: typescript: ^4.9.4 || ^5.0.2 zod: ^3 - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2102,64 +2388,62 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': + '@astrojs/check@0.9.4(typescript@5.9.2)': 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 + '@astrojs/language-server': 2.15.4(typescript@5.9.2) + chokidar: 4.0.3 kleur: 4.1.5 - typescript: 5.6.3 + typescript: 5.9.2 yargs: 17.7.2 transitivePeerDependencies: - prettier - prettier-plugin-astro - '@astrojs/compiler@2.10.3': {} + '@astrojs/compiler@2.13.0': {} - '@astrojs/internal-helpers@0.4.1': {} + '@astrojs/internal-helpers@0.7.2': {} - '@astrojs/language-server@2.15.4(typescript@5.6.3)': + '@astrojs/language-server@2.15.4(typescript@5.9.2)': dependencies: - '@astrojs/compiler': 2.10.3 + '@astrojs/compiler': 2.13.0 '@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 + '@jridgewell/sourcemap-codec': 1.5.5 + '@volar/kit': 2.4.23(typescript@5.9.2) + '@volar/language-core': 2.4.23 + '@volar/language-server': 2.4.23 + '@volar/language-service': 2.4.23 + fast-glob: 3.3.3 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 + volar-service-css: 0.0.62(@volar/language-service@2.4.23) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.23) + volar-service-html: 0.0.62(@volar/language-service@2.4.23) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.23) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.23) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.23) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.23) + vscode-html-languageservice: 5.5.1 + vscode-uri: 3.1.0 transitivePeerDependencies: - typescript - '@astrojs/markdown-remark@5.3.0': + '@astrojs/markdown-remark@6.3.6': dependencies: - '@astrojs/prism': 3.1.0 + '@astrojs/internal-helpers': 0.7.2 + '@astrojs/prism': 3.3.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 + import-meta-resolve: 4.2.0 + js-yaml: 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-gfm: 4.0.1 remark-parse: 11.0.0 - remark-rehype: 11.1.1 + remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 1.23.0 + shiki: 3.12.2 + smol-toml: 1.4.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 @@ -2168,24 +2452,43 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/prism@3.1.0': + '@astrojs/mdx@4.3.5(astro@5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1))': + dependencies: + '@astrojs/markdown-remark': 6.3.6 + '@mdx-js/mdx': 3.1.1 + acorn: 8.15.0 + astro: 5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1) + es-module-lexer: 1.7.0 + estree-util-visit: 2.0.0 + hast-util-to-html: 9.0.5 + kleur: 4.1.5 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-smartypants: 3.0.2 + source-map: 0.7.6 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.3.0': dependencies: - prismjs: 1.29.0 + prismjs: 1.30.0 - '@astrojs/tailwind@5.1.2(astro@4.16.13(rollup@4.27.2)(typescript@5.6.3))(tailwindcss@3.4.15)': + '@astrojs/tailwind@6.0.2(astro@5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17)': 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 + astro: 5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1) + autoprefixer: 10.4.21(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6) + tailwindcss: 3.4.17 transitivePeerDependencies: - ts-node - '@astrojs/telemetry@3.1.0': + '@astrojs/telemetry@3.3.0': dependencies: - ci-info: 4.1.0 - debug: 4.3.7 + ci-info: 4.3.0 + debug: 4.4.3 dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -2196,127 +2499,28 @@ snapshots: '@astrojs/yaml2ts@0.2.2': dependencies: - yaml: 2.6.0 + yaml: 2.8.1 - '@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.27.1': {} - '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} - '@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': + '@babel/parser@7.28.4': dependencies: - '@babel/types': 7.26.0 + '@babel/types': 7.28.4 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + '@babel/types@7.28.4': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 - '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': + '@capsizecss/unpack@2.4.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 + blob-to-buffer: 1.2.9 + cross-fetch: 3.2.0 + fontkit: 2.0.4 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 + - encoding '@emmetio/abbreviation@2.3.3': dependencies: @@ -2341,78 +2545,90 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} - '@emnapi/runtime@1.3.1': + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': optional: true - '@esbuild/android-arm64@0.21.5': + '@esbuild/darwin-arm64@0.25.10': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/darwin-x64@0.25.10': optional: true - '@esbuild/android-x64@0.21.5': + '@esbuild/freebsd-arm64@0.25.10': optional: true - '@esbuild/darwin-arm64@0.21.5': + '@esbuild/freebsd-x64@0.25.10': optional: true - '@esbuild/darwin-x64@0.21.5': + '@esbuild/linux-arm64@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.21.5': + '@esbuild/linux-arm@0.25.10': optional: true - '@esbuild/freebsd-x64@0.21.5': + '@esbuild/linux-ia32@0.25.10': optional: true - '@esbuild/linux-arm64@0.21.5': + '@esbuild/linux-loong64@0.25.10': optional: true - '@esbuild/linux-arm@0.21.5': + '@esbuild/linux-mips64el@0.25.10': optional: true - '@esbuild/linux-ia32@0.21.5': + '@esbuild/linux-ppc64@0.25.10': optional: true - '@esbuild/linux-loong64@0.21.5': + '@esbuild/linux-riscv64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.21.5': + '@esbuild/linux-s390x@0.25.10': optional: true - '@esbuild/linux-ppc64@0.21.5': + '@esbuild/linux-x64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/netbsd-arm64@0.25.10': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/netbsd-x64@0.25.10': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/openbsd-arm64@0.25.10': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.25.10': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openharmony-arm64@0.25.10': optional: true - '@esbuild/sunos-x64@0.21.5': + '@esbuild/sunos-x64@0.25.10': optional: true - '@esbuild/win32-arm64@0.21.5': + '@esbuild/win32-arm64@0.25.10': optional: true - '@esbuild/win32-ia32@0.21.5': + '@esbuild/win32-ia32@0.25.10': optional: true - '@esbuild/win32-x64@0.21.5': + '@esbuild/win32-x64@0.25.10': + optional: true + + '@img/colour@1.0.0': optional: true '@img/sharp-darwin-arm64@0.33.5': @@ -2420,101 +2636,214 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': optional: true '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.4': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.4': + 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: 7.1.2 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': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + acorn: 8.15.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color '@nodelib/fs.scandir@2.1.5': dependencies: @@ -2526,130 +2855,134 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.19.1 '@oslojs/encoding@1.1.0': {} '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/pluginutils@5.1.3(rollup@4.27.2)': + '@rollup/pluginutils@5.3.0(rollup@4.50.2)': dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.27.2 + rollup: 4.50.2 + + '@rollup/rollup-android-arm-eabi@4.50.2': + optional: true + + '@rollup/rollup-android-arm64@4.50.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.2': + optional: true - '@rollup/rollup-android-arm-eabi@4.27.2': + '@rollup/rollup-darwin-x64@4.50.2': optional: true - '@rollup/rollup-android-arm64@4.27.2': + '@rollup/rollup-freebsd-arm64@4.50.2': optional: true - '@rollup/rollup-darwin-arm64@4.27.2': + '@rollup/rollup-freebsd-x64@4.50.2': optional: true - '@rollup/rollup-darwin-x64@4.27.2': + '@rollup/rollup-linux-arm-gnueabihf@4.50.2': optional: true - '@rollup/rollup-freebsd-arm64@4.27.2': + '@rollup/rollup-linux-arm-musleabihf@4.50.2': optional: true - '@rollup/rollup-freebsd-x64@4.27.2': + '@rollup/rollup-linux-arm64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + '@rollup/rollup-linux-arm64-musl@4.50.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.2': + '@rollup/rollup-linux-loong64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.2': + '@rollup/rollup-linux-ppc64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.2': + '@rollup/rollup-linux-riscv64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + '@rollup/rollup-linux-riscv64-musl@4.50.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.2': + '@rollup/rollup-linux-s390x-gnu@4.50.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.2': + '@rollup/rollup-linux-x64-gnu@4.50.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.27.2': + '@rollup/rollup-linux-x64-musl@4.50.2': optional: true - '@rollup/rollup-linux-x64-musl@4.27.2': + '@rollup/rollup-openharmony-arm64@4.50.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.27.2': + '@rollup/rollup-win32-arm64-msvc@4.50.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.27.2': + '@rollup/rollup-win32-ia32-msvc@4.50.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.27.2': + '@rollup/rollup-win32-x64-msvc@4.50.2': optional: true - '@shikijs/core@1.23.0': + '@shikijs/core@3.12.2': dependencies: - '@shikijs/engine-javascript': 1.23.0 - '@shikijs/engine-oniguruma': 1.23.0 - '@shikijs/types': 1.23.0 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - hast-util-to-html: 9.0.3 + hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@1.23.0': + '@shikijs/engine-javascript@3.12.2': dependencies: - '@shikijs/types': 1.23.0 - '@shikijs/vscode-textmate': 9.3.0 - oniguruma-to-es: 0.1.2 + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 - '@shikijs/engine-oniguruma@1.23.0': + '@shikijs/engine-oniguruma@3.12.2': dependencies: - '@shikijs/types': 1.23.0 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/types@1.23.0': + '@shikijs/langs@3.12.2': dependencies: - '@shikijs/vscode-textmate': 9.3.0 - '@types/hast': 3.0.4 + '@shikijs/types': 3.12.2 - '@shikijs/vscode-textmate@9.3.0': {} + '@shikijs/themes@3.12.2': + dependencies: + '@shikijs/types': 3.12.2 - '@types/babel__core@7.20.5': + '@shikijs/types@3.12.2': 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 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 - '@types/babel__generator@7.6.8': + '@shikijs/vscode-textmate@10.0.2': {} + + '@swc/helpers@0.5.17': dependencies: - '@babel/types': 7.26.0 + tslib: 2.8.1 - '@types/babel__template@7.4.4': + '@types/debug@4.1.12': dependencies: - '@babel/parser': 7.26.2 - '@babel/types': 7.26.0 + '@types/ms': 2.1.0 - '@types/babel__traverse@7.20.6': + '@types/estree-jsx@1.0.5': dependencies: - '@babel/types': 7.26.0 + '@types/estree': 1.0.8 - '@types/cookie@0.6.0': {} + '@types/estree@1.0.8': {} - '@types/debug@4.1.12': + '@types/fontkit@2.0.8': dependencies: - '@types/ms': 0.7.34 - - '@types/estree@1.0.6': {} + '@types/node': 24.5.1 '@types/hast@3.0.4': dependencies: @@ -2659,72 +2992,84 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/ms@0.7.34': {} + '@types/mdx@2.0.13': {} + + '@types/ms@2.1.0': {} '@types/nlcst@2.0.3': dependencies: '@types/unist': 3.0.3 + '@types/node@24.5.1': + dependencies: + undici-types: 7.12.0 + + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} - '@ungap/structured-clone@1.2.0': {} + '@ungap/structured-clone@1.3.0': {} - '@volar/kit@2.4.10(typescript@5.6.3)': + '@volar/kit@2.4.23(typescript@5.9.2)': dependencies: - '@volar/language-service': 2.4.10 - '@volar/typescript': 2.4.10 + '@volar/language-service': 2.4.23 + '@volar/typescript': 2.4.23 typesafe-path: 0.2.2 - typescript: 5.6.3 + typescript: 5.9.2 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - '@volar/language-core@2.4.10': + '@volar/language-core@2.4.23': dependencies: - '@volar/source-map': 2.4.10 + '@volar/source-map': 2.4.23 - '@volar/language-server@2.4.10': + '@volar/language-server@2.4.23': dependencies: - '@volar/language-core': 2.4.10 - '@volar/language-service': 2.4.10 - '@volar/typescript': 2.4.10 + '@volar/language-core': 2.4.23 + '@volar/language-service': 2.4.23 + '@volar/typescript': 2.4.23 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 + vscode-uri: 3.1.0 - '@volar/language-service@2.4.10': + '@volar/language-service@2.4.23': dependencies: - '@volar/language-core': 2.4.10 + '@volar/language-core': 2.4.23 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - '@volar/source-map@2.4.10': {} + '@volar/source-map@2.4.23': {} - '@volar/typescript@2.4.10': + '@volar/typescript@2.4.23': dependencies: - '@volar/language-core': 2.4.10 + '@volar/language-core': 2.4.23 path-browserify: 1.0.1 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - '@vscode/emmet-helper@2.10.0': + '@vscode/emmet-helper@2.11.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-uri: 3.1.0 '@vscode/l10n@0.0.18': {} - acorn@8.14.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.0.3 + fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -2734,13 +3079,13 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} @@ -2751,84 +3096,102 @@ snapshots: 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): + astring@1.9.0: {} + + astro@5.13.8(@types/node@24.5.1)(jiti@1.21.7)(rollup@4.50.2)(typescript@5.9.2)(yaml@2.8.1): 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 + '@astrojs/compiler': 2.13.0 + '@astrojs/internal-helpers': 0.7.2 + '@astrojs/markdown-remark': 6.3.6 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 2.4.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 + '@rollup/pluginutils': 5.3.0(rollup@4.50.2) + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 boxen: 8.0.1 - ci-info: 4.1.0 + ci-info: 4.3.0 clsx: 2.1.1 common-ancestor-path: 1.0.1 - cookie: 0.7.2 + cookie: 1.0.2 cssesc: 3.0.0 - debug: 4.3.7 + debug: 4.4.3 deterministic-object-hash: 2.0.2 - devalue: 5.1.1 + devalue: 5.3.2 diff: 5.2.0 dlv: 1.1.3 dset: 3.1.4 - es-module-lexer: 1.5.4 - esbuild: 0.21.5 + es-module-lexer: 1.7.0 + esbuild: 0.25.10 estree-walker: 3.0.3 - fast-glob: 3.3.2 flattie: 1.1.1 + fontace: 0.3.0 github-slugger: 2.0.0 - gray-matter: 4.0.3 html-escaper: 3.0.3 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 js-yaml: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.12 + magic-string: 0.30.19 magicast: 0.3.5 - micromatch: 4.0.8 - mrmime: 2.0.0 + mrmime: 2.0.1 neotraverse: 0.6.18 - ora: 8.1.1 - p-limit: 6.1.0 - p-queue: 8.0.1 - preferred-pm: 4.0.0 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.3.0 + picomatch: 4.0.3 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) + semver: 7.7.2 + shiki: 3.12.2 + smol-toml: 1.4.2 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.2) + ultrahtml: 1.6.0 + unifont: 0.5.2 unist-util-visit: 5.0.0 + unstorage: 1.17.1 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 + vite: 6.3.6(@types/node@24.5.1)(jiti@1.21.7)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.3.6(@types/node@24.5.1)(jiti@1.21.7)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 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) + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.2)(zod@3.25.76) optionalDependencies: - sharp: 0.33.5 + sharp: 0.34.4 transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - encoding + - idb-keyval + - ioredis + - jiti - less - lightningcss - rollup @@ -2838,16 +3201,19 @@ snapshots: - sugarss - supports-color - terser + - tsx - typescript + - uploadthing + - yaml - autoprefixer@10.4.20(postcss@8.4.49): + autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.24.2 - caniuse-lite: 1.0.30001680 + browserslist: 4.26.2 + caniuse-lite: 1.0.30001743 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.4.49 + postcss: 8.5.6 postcss-value-parser: 4.2.0 axobject-query@4.1.0: {} @@ -2858,20 +3224,26 @@ snapshots: base-64@1.0.0: {} + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.5: {} + binary-extensions@2.3.0: {} + blob-to-buffer@1.2.9: {} + boxen@8.0.1: dependencies: ansi-align: 3.0.1 camelcase: 8.0.0 - chalk: 5.3.0 + chalk: 5.6.2 cli-boxes: 3.0.0 string-width: 7.2.0 - type-fest: 4.27.0 + type-fest: 4.41.0 widest-line: 5.0.0 - wrap-ansi: 9.0.0 + wrap-ansi: 9.0.2 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2879,22 +3251,27 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.2: + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserslist@4.26.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) + baseline-browser-mapping: 2.8.5 + caniuse-lite: 1.0.30001743 + electron-to-chromium: 1.5.220 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) camelcase-css@2.0.1: {} camelcase@8.0.0: {} - caniuse-lite@1.0.30001680: {} + caniuse-lite@1.0.30001743: {} ccount@2.0.1: {} - chalk@5.3.0: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -2902,6 +3279,8 @@ snapshots: character-entities@2.0.2: {} + character-reference-invalid@2.0.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -2914,28 +3293,26 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chokidar@4.0.1: + chokidar@4.0.3: dependencies: - readdirp: 4.0.2 + readdirp: 4.1.2 - ci-info@4.1.0: {} + ci-info@4.3.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 + clone@2.1.2: {} + clsx@2.1.1: {} + collapse-white-space@2.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2945,7 +3322,7 @@ snapshots: color-string@1.9.1: dependencies: color-name: 1.1.4 - simple-swizzle: 0.2.2 + simple-swizzle: 0.2.4 color@4.2.3: dependencies: @@ -2958,40 +3335,61 @@ snapshots: common-ancestor-path@1.0.1: {} - convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + + cookie@1.0.2: {} - cookie@0.7.2: {} + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding - cross-spawn@7.0.5: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + cssesc@3.0.0: {} - debug@4.3.7: + debug@4.4.3: dependencies: ms: 2.1.3 - decode-named-character-reference@1.0.2: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 + defu@6.1.4: {} + dequal@2.0.3: {} - detect-libc@2.0.3: {} + destr@2.0.5: {} + + detect-libc@2.1.0: {} deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 - devalue@5.1.1: {} + devalue@5.3.2: {} devlop@1.1.0: dependencies: dequal: 2.0.3 + dfa@1.2.0: {} + didyoumean@1.2.2: {} diff@5.2.0: {} @@ -3002,74 +3400,112 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.62: {} + electron-to-chromium@1.5.220: {} 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@10.5.0: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - entities@4.5.0: {} + entities@6.0.1: {} - es-module-lexer@1.5.4: {} + es-module-lexer@1.7.0: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 - esbuild@0.21.5: + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.15.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + esbuild@0.25.10: 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 + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 escalade@3.2.0: {} escape-string-regexp@5.0.0: {} - esprima@4.0.1: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 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: + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -3077,33 +3513,42 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-uri@3.0.3: {} + fast-uri@3.1.0: {} - fastq@1.17.1: + fastq@1.19.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - find-up-simple@1.0.0: {} + flattie@1.1.1: {} - find-up@4.1.0: + fontace@0.3.0: dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 + '@types/fontkit': 2.0.8 + fontkit: 2.0.4 - find-yarn-workspace-root2@1.2.16: + fontkit@2.0.4: dependencies: - micromatch: 4.0.8 - pkg-dir: 4.2.0 - - flattie@1.1.1: {} + '@swc/helpers': 0.5.17 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 - foreground-child@3.3.0: + foreground-child@3.3.1: dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 signal-exit: 4.1.0 fraction.js@4.3.7: {} @@ -3113,11 +3558,9 @@ snapshots: function-bind@1.1.2: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} + get-east-asian-width@1.4.0: {} github-slugger@2.0.0: {} @@ -3131,23 +3574,24 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.3.0 + foreground-child: 3.3.1 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: + h3@1.15.4: dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.3 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 hasown@2.0.2: dependencies: @@ -3157,18 +3601,18 @@ snapshots: dependencies: '@types/hast': 3.0.4 devlop: 1.1.0 - hast-util-from-parse5: 8.0.1 - parse5: 7.2.1 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 vfile: 6.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 - hast-util-from-parse5@8.0.1: + hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 devlop: 1.1.0 - hastscript: 8.0.0 - property-information: 6.5.0 + hastscript: 9.0.1 + property-information: 7.1.0 vfile: 6.0.3 vfile-location: 5.0.3 web-namespaces: 2.0.1 @@ -3185,19 +3629,40 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.2.0 - hast-util-from-parse5: 8.0.1 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - parse5: 7.2.1 + parse5: 7.3.0 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: + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 @@ -3206,11 +3671,31 @@ snapshots: hast-util-whitespace: 3.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - property-information: 6.5.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -3232,35 +3717,46 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hastscript@8.0.0: + hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 - property-information: 6.5.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 html-escaper@3.0.3: {} html-void-elements@3.0.0: {} - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.2.0: {} - import-meta-resolve@4.1.0: {} + import-meta-resolve@4.2.0: {} - is-arrayish@0.3.2: {} + inline-style-parser@0.2.4: {} + + iron-webcrypto@1.2.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.3.4: {} is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 - is-core-module@2.15.1: + is-core-module@2.16.1: dependencies: hasown: 2.0.2 - is-docker@3.0.0: {} + is-decimal@2.0.1: {} - is-extendable@0.1.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -3270,20 +3766,16 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.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 @@ -3296,77 +3788,44 @@ snapshots: 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 + jiti@1.21.7: {} 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: {} + lilconfig@3.1.3: {} 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: + magic-string@0.30.19: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 magicast@0.3.5: dependencies: - '@babel/parser': 7.26.2 - '@babel/types': 7.26.0 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 source-map-js: 1.2.1 + markdown-extensions@2.0.0: {} + markdown-table@3.0.4: {} mdast-util-definitions@6.0.0: @@ -3375,7 +3834,7 @@ snapshots: '@types/unist': 3.0.3 unist-util-visit: 5.0.0 - mdast-util-find-and-replace@3.0.1: + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 escape-string-regexp: 5.0.0 @@ -3386,15 +3845,15 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.1 + micromark: 4.0.2 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 + micromark-util-types: 2.0.2 unist-util-stringify-position: 4.0.0 transitivePeerDependencies: - supports-color @@ -3404,10 +3863,10 @@ snapshots: '@types/mdast': 4.0.4 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.1 + mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.0.0: + mdast-util-gfm-footnote@2.1.0: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 @@ -3444,11 +3903,11 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm@3.0.0: + mdast-util-gfm@3.1.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-footnote: 2.1.0 mdast-util-gfm-strikethrough: 2.0.0 mdast-util-gfm-table: 2.0.0 mdast-util-gfm-task-list-item: 2.0.0 @@ -3456,6 +3915,55 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@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-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@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-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -3465,7 +3973,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -3489,11 +3997,13 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} + merge2@1.4.1: {} - micromark-core-commonmark@2.0.2: + micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -3506,27 +4016,27 @@ snapshots: 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-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 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-util-types: 2.0.2 micromark-extension-gfm-footnote@2.1.0: dependencies: devlop: 1.1.0 - micromark-core-commonmark: 2.0.2 + micromark-core-commonmark: 2.0.3 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-util-types: 2.0.2 micromark-extension-gfm-strikethrough@2.1.0: dependencies: @@ -3535,19 +4045,19 @@ snapshots: 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-util-types: 2.0.2 - micromark-extension-gfm-table@2.1.0: + micromark-extension-gfm-table@2.1.1: 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-util-types: 2.0.2 micromark-extension-gfm-tagfilter@2.0.0: dependencies: - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-extension-gfm-task-list-item@2.1.0: dependencies: @@ -3555,55 +4065,118 @@ snapshots: 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-types: 2.0.2 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-table: 2.1.1 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-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 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-util-types: 2.0.2 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-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 micromark-factory-space@2.0.1: dependencies: micromark-util-character: 2.1.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 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-util-types: 2.0.2 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-types: 2.0.2 micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-chunked@2.0.1: dependencies: @@ -3613,12 +4186,12 @@ snapshots: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-combine-extensions@2.0.1: dependencies: micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-decode-numeric-character-reference@2.0.2: dependencies: @@ -3626,13 +4199,23 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.2.0 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-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + micromark-util-html-tag-name@2.0.1: {} micromark-util-normalize-identifier@2.0.1: @@ -3641,7 +4224,7 @@ snapshots: micromark-util-resolve-all@2.0.1: dependencies: - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-sanitize-uri@2.0.1: dependencies: @@ -3649,24 +4232,24 @@ snapshots: micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-subtokenize@2.0.2: + micromark-util-subtokenize@2.1.0: 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-types: 2.0.2 micromark-util-symbol@2.0.1: {} - micromark-util-types@2.0.1: {} + micromark-util-types@2.0.2: {} - micromark@4.0.1: + micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 - decode-named-character-reference: 1.0.2 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 - micromark-core-commonmark: 2.0.2 + micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-chunked: 2.0.1 @@ -3676,9 +4259,9 @@ snapshots: 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-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 transitivePeerDependencies: - supports-color @@ -3687,15 +4270,13 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mimic-function@5.0.1: {} - minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minipass@7.1.2: {} - mrmime@2.0.0: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -3707,7 +4288,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.7: {} + nanoid@3.3.11: {} neotraverse@0.6.18: {} @@ -3715,7 +4296,15 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 - node-releases@2.0.18: {} + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-mock-http@1.0.3: {} + + node-releases@2.0.21: {} normalize-path@3.0.0: {} @@ -3725,51 +4314,49 @@ snapshots: object-hash@3.0.0: {} - onetime@7.0.0: + ofetch@1.4.1: dependencies: - mimic-function: 5.0.1 + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 - oniguruma-to-es@0.1.2: - dependencies: - emoji-regex-xs: 1.0.0 - regex: 4.4.0 - regex-recursion: 4.2.1 + ohash@2.0.11: {} - 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 + oniguruma-parser@0.12.1: {} - p-limit@6.1.0: + oniguruma-to-es@4.3.3: dependencies: - yocto-queue: 1.1.1 + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 - p-locate@4.1.0: + p-limit@6.2.0: dependencies: - p-limit: 2.3.0 + yocto-queue: 1.2.1 - p-queue@8.0.1: + p-queue@8.1.1: dependencies: eventemitter3: 5.0.1 - p-timeout: 6.1.3 + p-timeout: 6.1.4 - p-timeout@6.1.3: {} - - p-try@2.2.0: {} + p-timeout@6.1.4: {} package-json-from-dist@1.0.1: {} + package-manager-detector@1.3.0: {} + + pako@0.2.9: {} + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -3779,14 +4366,12 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 - parse5@7.2.1: + parse5@7.3.0: dependencies: - entities: 4.5.0 + entities: 6.0.1 path-browserify@1.0.1: {} - path-exists@4.0.0: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -3800,40 +4385,34 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} pify@2.3.0: {} - pify@4.0.1: {} - - pirates@4.0.6: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 + pirates@4.0.7: {} - postcss-import@15.1.0(postcss@8.4.49): + postcss-import@15.1.0(postcss@8.5.6): dependencies: - postcss: 8.4.49 + postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.4.49): + postcss-js@4.1.0(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.49 + postcss: 8.5.6 - postcss-load-config@4.0.2(postcss@8.4.49): + postcss-load-config@4.0.2(postcss@8.5.6): dependencies: - lilconfig: 3.1.2 - yaml: 2.6.0 + lilconfig: 3.1.3 + yaml: 2.8.1 optionalDependencies: - postcss: 8.4.49 + postcss: 8.5.6 - postcss-nested@6.2.0(postcss@8.4.49): + postcss-nested@6.2.0(postcss@8.5.6): dependencies: - postcss: 8.4.49 + postcss: 8.5.6 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -3843,22 +4422,16 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.4.49: + postcss@8.5.6: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.11 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: {} + prismjs@1.30.0: {} prompts@2.4.2: dependencies: @@ -3867,8 +4440,12 @@ snapshots: property-information@6.5.0: {} + property-information@7.1.0: {} + queue-microtask@1.2.3: {} + radix3@1.1.2: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -3877,15 +4454,46 @@ snapshots: dependencies: picomatch: 2.3.1 - readdirp@4.0.2: {} + readdirp@4.1.2: {} - regex-recursion@4.2.1: + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 regex-utilities@2.3.0: {} - regex@4.4.0: {} + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 rehype-parse@9.0.1: dependencies: @@ -3899,10 +4507,18 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + rehype-stringify@10.0.1: dependencies: '@types/hast': 3.0.4 - hast-util-to-html: 9.0.3 + hast-util-to-html: 9.0.5 unified: 11.0.5 rehype@13.0.2: @@ -3912,10 +4528,10 @@ snapshots: rehype-stringify: 10.0.1 unified: 11.0.5 - remark-gfm@4.0.0: + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.0.0 + mdast-util-gfm: 3.1.0 micromark-extension-gfm: 3.0.0 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -3923,16 +4539,23 @@ snapshots: transitivePeerDependencies: - supports-color + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + 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 + micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-rehype@11.1.1: + remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -3961,16 +4584,13 @@ snapshots: require-from-string@2.0.2: {} - resolve@1.22.8: + resolve@1.22.10: dependencies: - is-core-module: 2.15.1 + is-core-module: 2.16.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 + restructure@3.0.2: {} retext-latin@4.0.0: dependencies: @@ -3997,50 +4617,46 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 - reusify@1.0.4: {} + reusify@1.1.0: {} - rollup@4.27.2: + rollup@4.50.2: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 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 + '@rollup/rollup-android-arm-eabi': 4.50.2 + '@rollup/rollup-android-arm64': 4.50.2 + '@rollup/rollup-darwin-arm64': 4.50.2 + '@rollup/rollup-darwin-x64': 4.50.2 + '@rollup/rollup-freebsd-arm64': 4.50.2 + '@rollup/rollup-freebsd-x64': 4.50.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.2 + '@rollup/rollup-linux-arm-musleabihf': 4.50.2 + '@rollup/rollup-linux-arm64-gnu': 4.50.2 + '@rollup/rollup-linux-arm64-musl': 4.50.2 + '@rollup/rollup-linux-loong64-gnu': 4.50.2 + '@rollup/rollup-linux-ppc64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-gnu': 4.50.2 + '@rollup/rollup-linux-riscv64-musl': 4.50.2 + '@rollup/rollup-linux-s390x-gnu': 4.50.2 + '@rollup/rollup-linux-x64-gnu': 4.50.2 + '@rollup/rollup-linux-x64-musl': 4.50.2 + '@rollup/rollup-openharmony-arm64': 4.50.2 + '@rollup/rollup-win32-arm64-msvc': 4.50.2 + '@rollup/rollup-win32-ia32-msvc': 4.50.2 + '@rollup/rollup-win32-x64-msvc': 4.50.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: {} + semver@7.7.2: {} sharp@0.33.5: dependencies: color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.3 + detect-libc: 2.1.0 + semver: 7.7.2 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -4062,36 +4678,68 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.0 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} - shiki@1.23.0: + shiki@3.12.2: 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 + '@shikijs/core': 3.12.2 + '@shikijs/engine-javascript': 3.12.2 + '@shikijs/engine-oniguruma': 3.12.2 + '@shikijs/langs': 3.12.2 + '@shikijs/themes': 3.12.2 + '@shikijs/types': 3.12.2 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 signal-exit@4.1.0: {} - simple-swizzle@0.2.2: + simple-swizzle@0.2.4: dependencies: - is-arrayish: 0.3.2 + is-arrayish: 0.3.4 sisteransi@1.0.5: {} - source-map-js@1.2.1: {} + smol-toml@1.4.2: {} - space-separated-tokens@2.0.2: {} + source-map-js@1.2.1: {} - sprintf-js@1.0.3: {} + source-map@0.7.6: {} - stdin-discarder@0.2.2: {} + space-separated-tokens@2.0.2: {} string-width@4.2.3: dependencies: @@ -4103,13 +4751,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 + emoji-regex: 10.5.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 stringify-entities@4.0.4: dependencies: @@ -4120,49 +4768,53 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 - strip-bom-string@1.0.0: {} + style-to-js@1.1.17: + dependencies: + style-to-object: 1.0.9 - strip-bom@3.0.0: {} + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 ts-interface-checker: 0.1.13 supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@3.4.15: + tailwindcss@3.4.17: 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 + fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 + jiti: 1.21.7 + lilconfig: 3.1.3 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: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 - resolve: 1.22.8 + resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node @@ -4175,34 +4827,60 @@ snapshots: dependencies: any-promise: 1.3.0 - tinyexec@0.3.1: {} + tiny-inflate@1.0.3: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + trim-lines@3.0.1: {} trough@2.2.0: {} ts-interface-checker@0.1.13: {} - tsconfck@3.1.4(typescript@5.6.3): + tsconfck@3.1.6(typescript@5.9.2): optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.2 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} - type-fest@4.27.0: {} + type-fest@4.41.0: {} typesafe-path@0.2.2: {} - typescript-auto-import-cache@0.3.5: + typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.6.3 + semver: 7.7.2 + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + ultrahtml@1.6.0: {} - typescript@5.6.3: {} + uncrypto@0.1.3: {} + + undici-types@7.12.0: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 unified@11.0.5: dependencies: @@ -4214,6 +4892,12 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unifont@0.5.2: + dependencies: + css-tree: 3.1.0 + ofetch: 1.4.1 + ohash: 2.0.11 + unist-util-find-after@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -4228,6 +4912,10 @@ snapshots: '@types/unist': 3.0.3 array-iterate: 2.0.1 + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-position@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -4256,9 +4944,20 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - update-browserslist-db@1.1.1(browserslist@4.24.2): + unstorage@1.17.1: + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.4 + lru-cache: 10.4.3 + node-fetch-native: 1.6.7 + ofetch: 1.4.1 + ufo: 1.6.1 + + update-browserslist-db@1.1.3(browserslist@4.26.2): dependencies: - browserslist: 4.24.2 + browserslist: 4.26.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -4269,7 +4968,7 @@ snapshots: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@4.0.2: + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 @@ -4277,88 +4976,94 @@ snapshots: vfile@6.0.3: dependencies: '@types/unist': 3.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 - vite@5.4.11: + vite@6.3.6(@types/node@24.5.1)(jiti@1.21.7)(yaml@2.8.1): dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.27.2 + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.50.2 + tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.5.1 fsevents: 2.3.3 + jiti: 1.21.7 + yaml: 2.8.1 - vitefu@1.0.3(vite@5.4.11): + vitefu@1.1.1(vite@6.3.6(@types/node@24.5.1)(jiti@1.21.7)(yaml@2.8.1)): optionalDependencies: - vite: 5.4.11 + vite: 6.3.6(@types/node@24.5.1)(jiti@1.21.7)(yaml@2.8.1) - volar-service-css@0.0.62(@volar/language-service@2.4.10): + volar-service-css@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-css-languageservice: 6.3.1 + vscode-css-languageservice: 6.3.7 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-emmet@0.0.62(@volar/language-service@2.4.10): + volar-service-emmet@0.0.62(@volar/language-service@2.4.23): dependencies: '@emmetio/css-parser': 0.4.0 '@emmetio/html-matcher': 1.3.0 - '@vscode/emmet-helper': 2.10.0 - vscode-uri: 3.0.8 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-html@0.0.62(@volar/language-service@2.4.10): + volar-service-html@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-html-languageservice: 5.3.1 + vscode-html-languageservice: 5.5.1 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-prettier@0.0.62(@volar/language-service@2.4.10): + volar-service-prettier@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.10): + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-typescript@0.0.62(@volar/language-service@2.4.10): + volar-service-typescript@0.0.62(@volar/language-service@2.4.23): dependencies: path-browserify: 1.0.1 - semver: 7.6.3 - typescript-auto-import-cache: 0.3.5 + semver: 7.7.2 + typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - volar-service-yaml@0.0.62(@volar/language-service@2.4.10): + volar-service-yaml@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 yaml-language-server: 1.15.0 optionalDependencies: - '@volar/language-service': 2.4.10 + '@volar/language-service': 2.4.23 - vscode-css-languageservice@6.3.1: + vscode-css-languageservice@6.3.7: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - vscode-html-languageservice@5.3.1: + vscode-html-languageservice@5.5.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-uri: 3.1.0 vscode-json-languageservice@4.1.8: dependencies: @@ -4366,7 +5071,7 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-nls: 5.2.0 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 vscode-jsonrpc@6.0.0: {} @@ -4398,15 +5103,18 @@ snapshots: vscode-nls@5.2.0: {} - vscode-uri@3.0.8: {} + vscode-uri@3.1.0: {} web-namespaces@2.0.1: {} - which-pm-runs@1.1.0: {} + webidl-conversions@3.0.1: {} - which-pm@3.0.0: + whatwg-url@5.0.0: dependencies: - load-yaml-file: 0.2.0 + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-pm-runs@1.1.0: {} which@2.0.2: dependencies: @@ -4424,22 +5132,20 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 - wrap-ansi@9.0.0: + wrap-ansi@9.0.2: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 7.2.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 - xxhash-wasm@1.0.2: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} - yallist@3.1.1: {} - yaml-language-server@1.15.0: dependencies: ajv: 8.17.1 @@ -4450,14 +5156,14 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-nls: 5.2.0 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 yaml: 2.2.2 optionalDependencies: prettier: 2.8.7 yaml@2.2.2: {} - yaml@2.6.0: {} + yaml@2.8.1: {} yargs-parser@21.1.1: {} @@ -4471,17 +5177,23 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yocto-queue@1.1.1: {} + yocto-queue@1.2.1: {} + + yocto-spinner@0.2.3: + dependencies: + yoctocolors: 2.1.2 + + yoctocolors@2.1.2: {} - zod-to-json-schema@3.23.5(zod@3.23.8): + zod-to-json-schema@3.24.6(zod@3.25.76): dependencies: - zod: 3.23.8 + zod: 3.25.76 - zod-to-ts@1.2.0(typescript@5.6.3)(zod@3.23.8): + zod-to-ts@1.2.0(typescript@5.9.2)(zod@3.25.76): dependencies: - typescript: 5.6.3 - zod: 3.23.8 + typescript: 5.9.2 + zod: 3.25.76 - zod@3.23.8: {} + zod@3.25.76: {} zwitch@2.0.4: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..d0b7dbe --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - esbuild + - sharp diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index da71f41..4df9bd2 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -1,13 +1,16 @@ --- -import Head, {type Props as HeadProps} from "./Head.astro"; +import NavBar from "../components/NavBar.astro"; +import Head, { type Props as HeadProps } from "./Head.astro"; type Props = { + navbar?: boolean; } & HeadProps; --- <html> -<Head {...Astro.props}></Head> -<body class="bg-gray-800 text-white"> -<slot/> -<footer></footer> -</body> + <Head {...Astro.props} /> + <body class="bg-gray-800 text-white"> + {(Astro.props.navbar ?? true) ? <NavBar /> : null} + <slot /> + <footer></footer> + </body> </html> diff --git a/web/src/pages/docs/_texture-pack-format.md b/web/src/pages/docs/_texture-pack-format.md index da66043..8cbedbf 100644 --- a/web/src/pages/docs/_texture-pack-format.md +++ b/web/src/pages/docs/_texture-pack-format.md @@ -139,11 +139,31 @@ Filter by item type: "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` to look at, separating sub elements with a `.`. You can use a `*` to check any child. +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). @@ -167,6 +187,32 @@ Sub object match: } ``` +#### 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 @@ -310,6 +356,58 @@ compare your number: 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. @@ -477,6 +575,251 @@ not screens from other mods. You can also target specific texts via a [string ma | `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). + +Signs can be targeted using `firmskyblock:sign` and `firmskyblock:hanging_sign`. + +### 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). + +Signs do not have a regular origin and are instead anchored from the top-middle of the sign. + +### 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. +- `signLines[]`: x, y, are available to move the text relative to where it would render normally. Must be an array of 4 component movers. + +### 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 | +| `<element>Title` | false | The title mover (see above for valid options) | +| `<element>Title.x` | false | The x coordinate of text relative to the top left of the screen | +| `<element>Title.y` | false | The y coordinate of text relative to the top left of the screen | +| `<element>Title.align` | false | How you want the text to align. "left", "center" or "right". This only changes the text direction, not its anchor point. | +| `<element>Title.replace` | false | Replace the text with your own text | +| `<extraComponent>` | false | Allows you to move button components and similar around | +| `<extraComponent>.x` | true | The new x coordinate of the component relative to the top left of the screen | +| `<extraComponent>.x` | true | The new y coordinate of the component relative to the top left of the screen | +| `<extraComponent>.width` | false | The new width of the component | +| `<extraComponent>.height` | false | The new height of the component | + +## Text Replacements + +> [!WARNING] +> This syntax is _experimental_ and may be reworked with no backwards compatibility guarantees. If you have a use case for this syntax, please contact me so that I can figure out what kind of features are needed for the final version of this API. + +Firmament allows you to replace arbitrary texts with other texts during rendering. This only affects rendering, not what other mods see. + +To do this, place a text override in `firmskyblock:overrides/texts/<my-override>.json`: + +```json +{ + "match": { + "regex": ".*Strength.*" + }, + "replacements": [ + { + "match": "❁", + "replace": { + "text": "<newIcon>", + "color": "#ff0000" + } + } + ] +} +``` + +There are notably two separate "match" sections. This is important. The first (top-level) match checks against the entire text element, while the replacement match operates on each individual subcomponent. Let's look at an example: + +```json +{ + "italic": false, + "text": "", + "extra": [ + " ", + { + "color": "red", + "text": "❁ Strength " + }, + { + "color": "white", + "text": "510.45" + } + ] +} +``` + +In this the entire text rendered out looks like `" ❁ Strength 510.45"` and the top-level match (`".*Strength.*"`) needs to match that line. + +Then each replacement (in the `replacements` array) is matched against each subcomponent. +First, it tries to find `"❁"` in the empty root element `""`. Then it tries the first child (`" "`) and fails again. Then it tries the `"❁ Strength "` component and finds one match. It then splits the `"❁ Strength "` component into multiple subsubcomponents and replaces just the `❁` part with the one specified in the replacements array. Afterwards, it fails to match the `"510.45"` component and returns. + +Our finalized text looks like this: + +```json +{ + "italic": false, + "text": "", + "extra": [ + " ", + { + "color": "red", + "text": "", + "extra": [ + { + "text": "<newIcon>", + "color": "#ff0000" + }, + " Strength " + ] + }, + { + "color": "white", + "text": "510.45" + } + ] +} +``` + +Which rendered out looks like ` <newIcon> Strength 510.45`, with all colours original, except the `<newIcon>` which not only has new text but also a new colour. + +| Field | Required | Description | +|--------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `match` | yes | A top level [string matcher](#string-matcher). Allows for testing parts of the text unrelated to the replacement and improves performance. | +| `replacements` | yes | A list of replacements to apply to each part of the text | +| `replacements.*.match` | yes | A [string matcher](#string-matcher) substring to replace in each component of the text. Notabene: Unlike most string matchers, this one is not anchored to the beginning and end of the element, so if the entire component needs to be matched a regex with `^$` needs to be used. | +| `replacements.*.style` | yes | A vanilla [style](https://minecraft.wiki/w/Text_component_format#Java_Edition) (where only the fields `color`, `italic`, `bold`, `underlined`, `strikethrough` and `obfuscated` are set). Checks if this specific subcomponent is of the correct style | +| `replacements.*.style.color` | no | A vanilla color name (as set in a text) that checks that the subcomponent is of that colour. | +| `replacements.*.style.italic` | no | A boolean that can be set `true` or `false` to require this text to be italic or not. | +| `replacements.*.style.bold` | no | A boolean that can be set `true` or `false` to require this text to be bold or not. | +| `replacements.*.style.underlined` | no | A boolean that can be set `true` or `false` to require this text to be underlined or not. | +| `replacements.*.style.strikethrough` | no | A boolean that can be set `true` or `false` to require this text to be strikethrough or not. | +| `replacements.*.style.obfuscated` | no | A boolean that can be set `true` or `false` to require this text to be obfuscated or not. | +| `replacements.*.replace` | yes | A vanilla [text](https://minecraft.wiki/w/Text_component_format#Java_Edition) that is inserted to replace the substring matched in the match. If literal texts (not translated texts) are used, then `${name}` can be used to access named groups in the match regex (if a regex matcher was used). | + + + ## 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 @@ -565,6 +908,11 @@ which block models are replaced under which conditions: } ``` +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/<namespace>/blockstates/<path>.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. | diff --git a/web/src/pages/docs/texture-pack-format.astro b/web/src/pages/docs/texture-pack-format.astro index 0714aea..ed14cde 100644 --- a/web/src/pages/docs/texture-pack-format.astro +++ b/web/src/pages/docs/texture-pack-format.astro @@ -6,7 +6,6 @@ import MarkDown from './_texture-pack-format.md'; --- <Base title="Firmament Texture Packs"> - <NavBar></NavBar> <Content> <MarkDown/> </Content> diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index 4baa867..f8c789f 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -6,7 +6,7 @@ import NavBar from "../components/NavBar.astro"; import Content from "../layouts/Content.astro"; --- -<Base title="Firmament"> +<Base title="Firmament" navbar={false}> <Hero image={Image} alt="A panorama shot of the HyPixel SkyBlock hub"> <h1 class="text-4xl font-bold leading-tight mb-4">Firmament</h1> <p class="text-lg text-gray-300 mb-8">Hypixel SkyBlock Utility Mod</p> diff --git a/web/src/pages/texture-packs.astro b/web/src/pages/texture-packs.astro index ee511f9..0f34157 100644 --- a/web/src/pages/texture-packs.astro +++ b/web/src/pages/texture-packs.astro @@ -4,6 +4,5 @@ import NavBar from "../components/NavBar.astro"; --- <Base title="Firmament Texture Packs"> - <NavBar></NavBar> </Base> diff --git a/web/tsconfig.json b/web/tsconfig.json index fbc2f5f..54f1732 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,3 +1,7 @@ { - "extends": "astro/tsconfigs/strictest" + "extends": "astro/tsconfigs/strictest", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false + } } |
