[
  {
    "path": ".gitattributes",
    "content": "* text eol=lf\n*.bat text eol=crlf\n*.patch text eol=lf\n*.java text eol=lf\n*.gradle text eol=crlf\n*.png binary\n*.gif binary\n*.exe binary\n*.dll binary\n*.jar binary\n*.lzma binary\n*.zip binary\n*.pyd binary\n*.cfg text eol=lf\n*.jks binary"
  },
  {
    "path": ".gitignore",
    "content": "# eclipse\nbin\n*.launch\n.settings\n.metadata\n.classpath\n.project\n\n# idea\nout\n*.ipr\n*.iws\n*.iml\n.idea/*\n!.idea/scopes\n\n# gradle\nbuild\n.gradle\n\n# other\neclipse\nrun\nruns\n.profileconfig.json"
  },
  {
    "path": "Jenkinsfile",
    "content": "#!/usr/bin/env groovy\n\npipeline {\n\n    agent any\n\n    tools {\n        jdk \"jdk-21\"\n    }\n\n    stages {\n\n        stage('Setup') {\n\n            steps {\n\n                echo 'Setup Project'\n                sh 'chmod +x gradlew'\n                sh './gradlew clean'\n            }\n        }\n\n        stage('Build') {\n\n            steps {\n\n                withCredentials([\n                    file(credentialsId: 'build_secrets', variable: 'ORG_GRADLE_PROJECT_secretFile'),\n                    file(credentialsId: 'java_keystore', variable: 'ORG_GRADLE_PROJECT_keyStore'),\n                    file(credentialsId: 'gpg_key', variable: 'ORG_GRADLE_PROJECT_pgpKeyRing')\n                ]) {\n\n                    echo 'Building project.'\n                    sh './gradlew build publish publishCurseForge modrinth updateVersionTracker --stacktrace --warn'\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\nCopyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]\n\n                            Preamble\n\nThe licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU\nGeneral Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the\nsoftware is free for all its users.\n\nThis license, the Lesser General Public License, applies to some specially designated software packages--typically\nlibraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest\nyou first think carefully about whether this license or the ordinary General Public License is the better strategy to\nuse in any particular case, based on the explanations below.\n\nWhen we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed\nto make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish);\nthat you receive source code or can get it if you want it; that you can change the software and use pieces of it in new\nfree programs; and that you are informed that you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to\nsurrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the\nlibrary or if you modify it.\n\nFor example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the\nrights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code\nwith the library, you must provide complete object files to the recipients, so that they can relink them with the\nlibrary after making changes to the library and recompiling it. And you must show them these terms so they know their\nrights.\n\nWe protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which\ngives you legal permission to copy, distribute and/or modify the library.\n\nTo protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the\nlibrary is modified by someone else and passed on, the recipients should know that what they have is not the original\nversion, so that the original author's reputation will not be affected by problems that might be introduced by others.\nFinally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a\ncompany cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder.\nTherefore, we insist that any patent license obtained for a version of the library must be consistent with the full\nfreedom of use specified in this license.\n\nMost GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the\nGNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary\nGeneral Public License. We use this license for certain libraries in order to permit linking those libraries into\nnon-free programs.\n\nWhen a program is linked with a library, whether statically or using a shared library, the combination of the two is\nlegally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore\npermits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License\npermits more lax criteria for linking other code with the library.\n\nWe call this license the \"Lesser\" General Public License because it does Less to protect the user's freedom than the\nordinary General Public License. It also provides other free software developers Less of an advantage over competing\nnon-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries.\nHowever, the Lesser license provides advantages in certain special circumstances.\n\nFor example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library,\nso that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more\nfrequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little\nto gain by limiting the free library to free software only, so we use the Lesser General Public License.\n\nIn other cases, permission to use a particular library in non-free programs enables a greater number of people to use a\nlarge body of free software. For example, permission to use the GNU C Library in non-free programs enables many more\npeople to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.\n\nAlthough the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a\nprogram that is linked with the Library has the freedom and the wherewithal to run that program using a modified version\nof the Library.\n\nThe precise terms and conditions for copying, distribution and modification follow. Pay close attention to the\ndifference between a\n\"work based on the library\" and a \"work that uses the library\". The former contains code derived from the library,\nwhereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND\nCONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License Agreement applies to any software library or other program which contains a notice placed by the\n   copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public\n   License (also called \"this License\"). Each licensee is addressed as \"you\".\n\nA \"library\" means a collection of software functions and/or data prepared so as to be conveniently linked with\napplication programs\n(which use some of those functions and data) to form executables.\n\nThe \"Library\", below, refers to any such software library or work which has been distributed under these terms. A \"work\nbased on the Library\" means either the Library or any derivative work under copyright law: that is to say, a work\ncontaining the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly\ninto another language.  (Hereinafter, translation is included without limitation in the term \"modification\".)\n\n\"Source code\" for a work means the preferred form of the work for making modifications to it. For a library, complete\nsource code means all the source code for all modules it contains, plus any associated interface definition files, plus\nthe scripts used to control compilation and installation of the library.\n\nActivities other than copying, distribution and modification are not covered by this License; they are outside its\nscope. The act of running a program using the Library is not restricted, and output from such a program is covered only\nif its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it).\nWhether that is true depends on what the Library does and what the program that uses the Library does.\n\n1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium,\n   provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer\n   of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and\n   distribute a copy of this License along with the Library.\n\nYou may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection\nin exchange for a fee.\n\n2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and\n   copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of\n   these conditions:\n\n   a) The modified work must itself be a software library.\n\n   b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of\n   any change.\n\n   c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this\n   License.\n\n   d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application\n   program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a\n   good faith effort to ensure that, in the event an application does not supply such function or table, the facility\n   still operates, and performs whatever part of its purpose remains meaningful.\n\n   (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent\n   of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this\n   function must be optional: if the application does not supply it, the square root function must still compute square\n   roots.)\n\nThese requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the\nLibrary, and can be reasonably considered independent and separate works in themselves, then this License, and its\nterms, do not apply to those sections when you distribute them as separate works. But when you distribute the same\nsections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the entire whole, and thus to each and every part\nregardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you;\nrather, the intent is to exercise the right to control the distribution of derivative or collective works based on the\nLibrary.\n\nIn addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the\nLibrary) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.\n\n3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of\n   the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the\n   ordinary GNU General Public License, version 2, instead of to this License.  (If a newer version than version 2 of\n   the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.)  Do not\n   make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy,\n   so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.\n\nThis option is useful when you wish to copy part of the code of the Library into a program that is not a library.\n\n4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or\n   executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete\n   corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a\n   medium customarily used for software interchange.\n\nIf distribution of object code is made by offering access to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place satisfies the requirement to distribute the source code, even though\nthird parties are not compelled to copy the source along with the object code.\n\n5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by\n   being compiled or linked with it, is called a \"work that uses the Library\". Such a work, in isolation, is not a\n   derivative work of the Library, and therefore falls outside the scope of this License.\n\nHowever, linking a \"work that uses the Library\" with the Library creates an executable that is a derivative of the\nLibrary (because it contains portions of the Library), rather than a \"work that uses the library\". The executable is\ntherefore covered by this License. Section 6 states terms for distribution of such executables.\n\nWhen a \"work that uses the Library\" uses material from a header file that is part of the Library, the object code for\nthe work may be a derivative work of the Library even though the source code is not. Whether this is true is especially\nsignificant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to\nbe true is not precisely defined by law.\n\nIf such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small\ninline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether\nit is legally a derivative work.  (Executables containing this object code plus portions of the Library will still fall\nunder Section 6.)\n\nOtherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms\nof Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly\nwith the Library itself.\n\n6. As an exception to the Sections above, you may also combine or link a \"work that uses the Library\" with the Library\n   to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided\n   that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such\n   modifications.\n\nYou must give prominent notice with each copy of the work that the Library is used in it and that the Library and its\nuse are covered by this License. You must supply a copy of this License. If the work during execution displays copyright\nnotices, you must include the copyright notice for the Library among them, as well as a reference directing the user to\nthe copy of this License. Also, you must do one of these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\nFor an executable, the required form of the \"work that uses the Library\" must include any data and utility programs\nneeded for reproducing the executable from it. However, as a special exception, the materials to be distributed need not\ninclude anything that is normally distributed (in either source or binary form) with the major components (compiler,\nkernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the\nexecutable.\n\nIt may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not\nnormally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in\nan executable that you distribute.\n\n7. You may place library facilities that are a work based on the Library side-by-side in a single library together with\n   other library facilities not covered by this License, and distribute such a combined library, provided that the\n   separate distribution of the work based on the Library and of the other library facilities is otherwise permitted,\n   and provided that you do these two things:\n\n   a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other\n   library facilities. This must be distributed under the terms of the Sections above.\n\n   b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and\n   explaining where to find the accompanying uncombined form of the same work.\n\n8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this\n   License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will\n   automatically terminate your rights under this License. However, parties who have received copies, or rights, from\n   you under this License will not have their licenses terminated so long as such parties remain in full compliance.\n\n9. You are not required to accept this License, since you have not signed it. However, nothing else grants you\n   permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do\n   not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you\n   indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or\n   modifying the Library or works based on it.\n\n10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a\n    license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and\n    conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein.\n    You are not responsible for enforcing compliance by third parties with this License.\n\n11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited\n    to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict\n    the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute\n    so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a\n    consequence you may not distribute the Library at all. For example, if a patent license would not permit\n    royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then\n    the only way you could satisfy both it and this License would be to refrain entirely from distribution of the\n    Library.\n\nIf any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the\nsection is intended to apply, and the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest\nvalidity of any such claims; this section has the sole purpose of protecting the integrity of the free software\ndistribution system which is implemented by public license practices. Many people have made generous contributions to\nthe wide range of software distributed through that system in reliance on consistent application of that system; it is\nup to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee\ncannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.\n\n12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted\n    interfaces, the original copyright holder who places the Library under this License may add an explicit geographical\n    distribution limitation excluding those countries, so that distribution is permitted only in or among countries not\n    thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.\n\n13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time\n    to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new\n    problems or concerns.\n\nEach version is given a distinguishing version number. If the Library specifies a version number of this License which\napplies to it and\n\"any later version\", you have the option of following the terms and conditions either of that version or of any later\nversion published by the Free Software Foundation. If the Library does not specify a license version number, you may\nchoose any version ever published by the Free Software Foundation.\n\n14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are\n    incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free\n    Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will\n    be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting\n    the sharing and reuse of software generally.\n\n                          NO WARRANTY\n\n15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY\n    APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE\n    LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\n    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND\n    PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY\n    SERVICING, REPAIR OR CORRECTION.\n\n16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY\n    WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n    GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (\n    INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n    PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN\n    ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n                   END OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Libraries\n\nIf you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it\nfree software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (\nor, alternatively, under the terms of the ordinary General Public License).\n\nTo apply these terms, attach the following notices to the library. It is safest to attach them to the start of each\nsource file to most effectively convey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your school, if any, to sign a \"copyright disclaimer\"\nfor the library, if necessary. Here is a sample; alter the names:\n\nYoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by\nJames Random Hacker.\n\n<signature of Ty Coon>, 1 April 1990 Ty Coon, President of Vice\n\nThat's all there is to it!"
  },
  {
    "path": "README.md",
    "content": "<!-- name-start -->\n# Bookshelf [![CurseForge Project](https://img.shields.io/curseforge/dt/228525?logo=curseforge&label=CurseForge&style=flat-square&labelColor=2D2D2D&color=555555)](https://www.curseforge.com/minecraft/mc-mods/bookshelf) [![Modrinth Project](https://img.shields.io/modrinth/dt/uy4Cnpcm?logo=modrinth&label=Modrinth&style=flat-square&labelColor=2D2D2D&color=555555)](https://modrinth.com/mod/bookshelf-lib) [![Maven Project](https://img.shields.io/maven-metadata/v?style=flat-square&logoColor=D31A38&labelColor=2D2D2D&color=555555&label=Latest&logo=gradle&metadataUrl=https%3A%2F%2Fmaven.blamejared.com%2Fnet%2Fdarkhax%2Fbookshelf%2Fbookshelf-common-1.21.1%2Fmaven-metadata.xml)](https://maven.blamejared.com/net/darkhax/bookshelf)\n<!-- name-end -->\n<!-- description-start -->\nBookshelf is a library mod that provides code, frameworks, and utilities for other mods. Many mods make use of Bookshelf and are powered by its code. The documentation for this mod can be found [here](https://docs.darkhax.net/mods/bookshelf).\n<!-- description-end -->\n\n## Why use a library mod?\nLibrary mods like Bookshelf allow seemingly unrelated mods to reuse parts\nof the same code base. This reduces the amount of time required to develop\nand maintain certain mods and features. Library code is also tested in a\nwider range of circumstances and communities which can lead to fewer bugs\nand faster code.\n\n## Built With Bookshelf\nThe following mods were built using Bookshelf and are powered by its code!\n\n- [Enchantment Descriptions](https://www.curseforge.com/minecraft/mc-mods/enchantment-descriptions) - Adds in-game descriptions for enchantments to tooltips.\n- [Botany Pots](https://www.curseforge.com/minecraft/mc-mods/botany-pots) - Adds pots that you can use to grow crops!\n- [Tips](https://www.curseforge.com/minecraft/mc-mods/tips) - Adds tips to various loading screens.\n- [Dark Utilities](https://www.curseforge.com/minecraft/mc-mods/dark-utilities) - Blocks and items with interesting effects and abilities.\n\n<!-- maven-start -->\n## Maven Dependency\n\nIf you are using [Gradle](https://gradle.org) to manage your dependencies, add the following into your `build.gradle` file. Make sure to replace the version with the correct one. All versions can be viewed [here](https://maven.blamejared.com/net/darkhax/bookshelf).\n\n```gradle\nrepositories {\n    maven { \n        url 'https://maven.blamejared.com'\n    }\n}\n\ndependencies {\n    // NeoForge\n    implementation group: 'net.darkhax.bookshelf', name: 'bookshelf-neoforge-1.21.1', version: '21.1.0'\n\n    // Forge\n    implementation group: 'net.darkhax.bookshelf', name: 'bookshelf-forge-1.21.1', version: '21.1.0'\n\n    // Fabric & Quilt\n    modImplementation group: 'net.darkhax.bookshelf', name: 'bookshelf-fabric-1.21.1', version: '21.1.0'\n\n    // Common / MultiLoader / Vanilla\n    compileOnly group: 'net.darkhax.bookshelf', name: 'bookshelf-common-1.21.1', version: '21.1.0'\n}\n```\n<!-- maven-end -->\n\n<!-- sponsor-start -->\n## Sponsors\n\n[![](https://assets.blamejared.com/nodecraft/darkhax.jpg)](https://nodecraft.com/r/darkhax)    \nBookshelf is sponsored by Nodecraft. Use code **[DARKHAX](https://nodecraft.com/r/darkhax)** for 30% of your first month of service!\n<!-- sponsor-end -->"
  },
  {
    "path": "build.gradle",
    "content": "plugins {\r\n    id 'secret-loader'\r\n    id 'fabric-loom' version '1.11-SNAPSHOT' apply false\r\n    id 'net.neoforged.moddev' version '2.0.112' apply false\r\n    id 'net.darkhax.curseforgegradle' version '1.1.25' apply(false)\r\n    id 'com.modrinth.minotaur' version '2.8.7' apply(false)\r\n    id 'project_validation'\r\n    id 'build-number'\r\n    id 'git-changelog'\r\n    id 'patreon'\r\n    id 'version-checker'\r\n    id 'readme-update'\r\n}"
  },
  {
    "path": "buildSrc/build.gradle",
    "content": "plugins {\r\n    id 'groovy-gradle-plugin'\r\n}\r\n\r\nrepositories {\r\n    mavenCentral()\r\n}\r\n\r\ndependencies {\r\n    implementation group: 'com.diluv.schoomp', name: 'Schoomp', version: '1.2.6'\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/build-number.gradle",
    "content": "// This script attempts to append the build number to the project version.\r\n// The build number is read from an environment variable that is set by\r\n// a CI like Jenkins. If the build number is missing it will default to 0.\r\nvar buildNumber = System.getenv('BUILD_NUMBER') ? System.getenv('BUILD_NUMBER') : 0\r\nproject.version = \"${project.version}.${buildNumber}\".toString()\r\nproject.getSubprojects().each { proj -> proj.version = project.version }\r\nproject.logger.lifecycle(\"Appending build number to version. Build #${buildNumber} Version is now ${project.version}\")"
  },
  {
    "path": "buildSrc/src/main/groovy/git-changelog.gradle",
    "content": "project.ext.mod_changelog = 'No changelog was provided. Please refer to the project page for more information.'\r\n\r\ntry {\r\n    project.mod_changelog = 'No changelog was provided. Please refer to the project page for more information.'\r\n    def gitCommit = System.getenv('GIT_COMMIT') ?: getExecOutput(['git', 'log', '-n', '1', '--pretty=tformat:%h'])\r\n    def gitPrevCommit = System.getenv('GIT_PREVIOUS_COMMIT')\r\n\r\n    // If a full range is available use that range.\r\n    if (gitCommit && gitPrevCommit) {\r\n        project.ext.mod_changelog = getExecOutput(['git', 'log', \"--pretty=tformat:- %s\", '' + gitPrevCommit + '..' + gitCommit])\r\n        project.logger.lifecycle(\"Generated changelog using commits ${gitPrevCommit} to ${gitCommit}.\")\r\n    }\r\n\r\n    // If only one commit is available, use the last commit.\r\n    else if (gitCommit) {\r\n        project.ext.mod_changelog = getExecOutput(['git', 'log', '' + \"--pretty=tformat:- %s\", '-1', '' + gitCommit])\r\n        project.logger.lifecycle(\"Generated changelog using commit ${gitCommit}.\")\r\n    }\r\n}\r\n\r\ncatch (Exception e) {\r\n\r\n    project.logger.warn(\"Changelogs could not be generated! ${e.message}\")\r\n}\r\n\r\ndef getExecOutput(commands) {\r\n\r\n    def out = new ByteArrayOutputStream()\r\n\r\n    exec {\r\n        commandLine commands\r\n        standardOutput out\r\n    }\r\n\r\n    return out.toString().trim()\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/minify-json.gradle",
    "content": "// This script will minify JSON files as the project is built. This is\r\n// done by removing unused whitespace and newlines from the file. The\r\n// original source file is not modified.\r\n//\r\n// Minifying JSON files will produce a smaller JAR file. This will make\r\n// upload/download times faster, reduce bandwidth usage, and reduce the\r\n// amount of storage space required to use or host the project.\r\n//\r\n// Minified JSON files are also faster to read and parse. The reduced file\r\n// size makes them faster to stream from disk, and the JSON tokenizer does\r\n// not need to waste cycles handling unnecessary data.\r\nimport groovy.json.JsonOutput\r\nimport groovy.json.JsonSlurper\r\n\r\nprocessResources {\r\n\r\n    doLast {\r\n\r\n        def jsonMinifyStart = System.currentTimeMillis()\r\n        def jsonMinified = 0\r\n        def jsonBytesSaved = 0\r\n\r\n        fileTree(dir: outputs.files.asPath, include: ['**/*.json', '**/*.mcmeta']).each {\r\n\r\n            try {\r\n                def oldLength = it.length()\r\n                it.text = JsonOutput.toJson(new JsonSlurper().parse(it))\r\n                jsonBytesSaved += oldLength - it.length()\r\n                jsonMinified++\r\n            }\r\n\r\n            catch (Exception e) {\r\n\r\n                project.logger.error(\"Failed to minify file '${it.path}'.\")\r\n                throw e\r\n            }\r\n        }\r\n\r\n        project.logger.lifecycle(\"Minified ${jsonMinified} files. Saved ${jsonBytesSaved} bytes before compression. Took ${System.currentTimeMillis() - jsonMinifyStart}ms.\")\r\n    }\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/multiloader-common.gradle",
    "content": "plugins {\r\n    id 'java-library'\r\n    id 'maven-publish'\r\n    id 'minify-json'\r\n}\r\n\r\nbase {\r\n    archivesName = \"${mod_id}-${project.name}-${minecraft_version}\"\r\n}\r\n\r\njava {\r\n    toolchain.languageVersion = JavaLanguageVersion.of(java_version)\r\n    withSourcesJar()\r\n    withJavadocJar()\r\n}\r\n\r\njavadoc {\r\n    options.addStringOption('Xdoclint:-missing', '-quiet')\r\n}\r\n\r\nrepositories {\r\n    mavenCentral()\r\n    exclusiveContent {\r\n        forRepository {\r\n            maven {\r\n                name = 'Sponge'\r\n                url = 'https://repo.spongepowered.org/repository/maven-public'\r\n            }\r\n        }\r\n        filter {\r\n            includeGroupAndSubgroups('org.spongepowered')\r\n        }\r\n    }\r\n    exclusiveContent {\r\n        forRepositories(\r\n                maven {\r\n                    name = 'ParchmentMC'\r\n                    url = 'https://maven.parchmentmc.org/'\r\n                },\r\n                maven {\r\n                    name = \"NeoForge\"\r\n                    url = 'https://maven.neoforged.net/releases'\r\n                }\r\n        )\r\n        filter {\r\n            includeGroup('org.parchmentmc.data')\r\n        }\r\n    }\r\n    exclusiveContent {\r\n        forRepository {\r\n            maven {\r\n                url \"https://cursemaven.com\"\r\n            }\r\n        }\r\n        filter {\r\n            includeGroup \"curse.maven\"\r\n        }\r\n    }\r\n    maven {\r\n        name = 'BlameJared'\r\n        url = 'https://maven.blamejared.com'\r\n    }\r\n}\r\n\r\n['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant ->\r\n    configurations.\"$variant\".outgoing {\r\n        capability(\"$group:${base.archivesName.get()}:$version\")\r\n        capability(\"$group:$mod_id-${project.name}-${minecraft_version}:$version\")\r\n        capability(\"$group:$mod_id:$version\")\r\n    }\r\n    publishing.publications.configureEach {\r\n        suppressPomMetadataWarningsFor(variant)\r\n    }\r\n}\r\n\r\nsourcesJar {\r\n    from(rootProject.file('LICENSE')) {\r\n        rename { \"license_${mod_id}.txt\" }\r\n    }\r\n}\r\n\r\njar {\r\n    from(rootProject.file('LICENSE')) {\r\n        rename { \"license_${mod_id}.txt\" }\r\n    }\r\n\r\n    manifest {\r\n        attributes([\r\n                'Specification-Title'   : mod_name,\r\n                'Specification-Vendor'  : mod_author,\r\n                'Specification-Version' : project.jar.archiveVersion,\r\n                'Implementation-Title'  : project.name,\r\n                'Implementation-Version': project.jar.archiveVersion,\r\n                'Implementation-Vendor' : mod_author,\r\n                'Built-On-Minecraft'    : minecraft_version,\r\n                'CurseForge'            : curse_page,\r\n                'Modrinth'              : modrinth_page\r\n        ])\r\n    }\r\n}\r\n\r\nprocessResources {\r\n    var expandProps = [\r\n            'version'                      : project.version,\r\n            'group'                        : project.group,\r\n            'platform'                     : project.name,\r\n            'minecraft_version'            : minecraft_version,\r\n            'minecraft_version_range'      : minecraft_version_range,\r\n            'mod_name'                     : mod_name,\r\n            'mod_author'                   : mod_author,\r\n            'mod_id'                       : mod_id,\r\n            'mod_repo'                     : mod_repo,\r\n            'mod_license'                  : mod_license,\r\n            'mod_description'              : mod_description,\r\n            'mod_item_icon'                : mod_item_icon,\r\n            'neoforge_version'             : neoforge_version,\r\n            'neoforge_loader_version_range': neoforge_loader_version_range,\r\n            'fabric_version'               : fabric_version,\r\n            'fabric_loader_version'        : fabric_loader_version,\r\n            'java_version'                 : java_version,\r\n            'curse_project'                : curse_project,\r\n            'curse_page'                   : curse_page,\r\n            'modrinth_project'             : modrinth_project,\r\n            'modrinth_page'                : modrinth_page,\r\n            'mod_client_only'              : mod_client_only,\r\n            'patreon_pledges'              : rootProject.ext.patreon.pledgeNames,\r\n            'patreon_url'                  : rootProject.ext.patreon.campaignUrl\r\n    ]\r\n\r\n    boolean clientOnly = project.hasProperty('mod_client_only') && project.findProperty('mod_client_only') == 'true'\r\n\r\n    if ('fabric' == project.name) {\r\n        expandProps.put('mod_target_environment', clientOnly ? 'client' : '*')\r\n        expandProps.put('mod_target_environment', clientOnly ? 'client' : '*')\r\n    }\r\n\r\n    filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'META-INF/neoforge.mods.toml', '*.mixins.json']) {\r\n        expand expandProps\r\n    }\r\n    inputs.properties(expandProps)\r\n}\r\n\r\npublishing {\r\n    publications {\r\n        register('mavenJava', MavenPublication) {\r\n            artifactId base.archivesName.get()\r\n            from components.java\r\n        }\r\n    }\r\n    repositories {\r\n        maven {\r\n            url System.getenv('local_maven_url')\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/multiloader-loader.gradle",
    "content": "plugins {\r\n    id 'multiloader-common'\r\n}\r\n\r\nconfigurations {\r\n    commonJava{\r\n        canBeResolved = true\r\n    }\r\n    commonResources{\r\n        canBeResolved = true\r\n    }\r\n}\r\n\r\ndependencies {\r\n    compileOnly(project(':common')) {\r\n        capabilities {\r\n            requireCapability \"$group:$mod_id\"\r\n        }\r\n    }\r\n    commonJava project(path: ':common', configuration: 'commonJava')\r\n    commonResources project(path: ':common', configuration: 'commonResources')\r\n}\r\n\r\ntasks.named('compileJava', JavaCompile) {\r\n    dependsOn(configurations.commonJava)\r\n    source(configurations.commonJava)\r\n}\r\n\r\nprocessResources {\r\n    dependsOn(configurations.commonResources)\r\n    from(configurations.commonResources)\r\n}\r\n\r\ntasks.named('javadoc', Javadoc).configure {\r\n    dependsOn(configurations.commonJava)\r\n    source(configurations.commonJava)\r\n}\r\n\r\ntasks.named('sourcesJar', Jar) {\r\n    dependsOn(configurations.commonJava)\r\n    from(configurations.commonJava)\r\n    dependsOn(configurations.commonResources)\r\n    from(configurations.commonResources)\r\n}\r\n"
  },
  {
    "path": "buildSrc/src/main/groovy/patreon.gradle",
    "content": "import groovy.json.JsonSlurper\r\n\r\nproject.ext.patreon = [\r\n        pledgeNames       : '',\r\n        campaignUrl       : ''\r\n]\r\n\r\nproject.ext.mod_supporters = 'No supporters loaded.'\r\n\r\nif (project.hasProperty('patreon_campaign_id') && project.hasProperty('patreon_auth_token')) {\r\n\r\n    project.ext.patreon.campaign = project.findProperty('patreon_campaign_id')\r\n    def authToken = project.findProperty('patreon_auth_token')\r\n    getPledges(project.ext.patreon.campaign, authToken)\r\n    if (project.hasProperty('patreon_campaign_url')) {\r\n        project.ext.patreon.campaignUrl = project.getProperty('patreon_campaign_url')\r\n    }\r\n    project.logger.lifecycle(\"Loading pledge data for default campaign ${project.ext.patreon.campaign}.\")\r\n} else {\r\n    project.logger.warn(\"Patreon data can not be loaded! has_id:${project.hasProperty('patreon_campaign_id')} has_campaign:${project.hasProperty('patreon_auth_token')}\")\r\n}\r\n\r\n/*\r\n Gets a list of pledges for a specified campaign using a specified auth token.\r\n*/\r\n\r\ndef getPledges(campaignId, authToken) {\r\n\r\n    // Connect to the Patreon API using the provided auth info.\r\n    def connection = new URL('https://www.patreon.com/api/oauth2/api/campaigns/' + campaignId + '/pledges').openConnection() as HttpURLConnection\r\n    connection.setRequestProperty('User-Agent', 'Patreon-Groovy, platform ' + System.properties['os.name'] + ' ' + System.properties['os.version'])\r\n    connection.setRequestProperty('Authorization', 'Bearer ' + authToken)\r\n    connection.setRequestProperty('Accept', 'application/json')\r\n\r\n    // Check if connection was valid.\r\n    if (connection.responseCode == 200) {\r\n\r\n        // Map holding pledge data. The key is a string representation of the\r\n        // users ID and the value is an object holding information about the\r\n        // pledge.\r\n        Map<String, Pledge> pledges = new HashMap<String, Pledge>()\r\n\r\n        // Parse the response into an ambiguous json object.\r\n        def json = connection.inputStream.withCloseable { inStream -> new JsonSlurper().parse(inStream as InputStream) }\r\n\r\n        // Iterate all the pledge entries\r\n        for (pledgeInfo in json.data) {\r\n\r\n            // Create new pledge entry, and set pledge specific info.\r\n            def pledge = new Pledge()\r\n            pledge.id = pledgeInfo.relationships.patron.data.id\r\n            pledge.amountInCents = pledgeInfo.attributes.amount_cents\r\n            pledge.declined = pledgeInfo.attributes.declined_since\r\n\r\n            if (pledge.isValid()) {\r\n                pledges.put(pledge.id, pledge)\r\n            }\r\n        }\r\n\r\n        // Parse out the pledges display info from the JSON.\r\n        for (pledgeInfo in json.included) {\r\n\r\n            // Get pledge by user ID\r\n            def pledge = pledges.get(pledgeInfo.id)\r\n\r\n            // If the pledge exists, set the user data.\r\n            if (pledge != null) {\r\n                def info = pledgeInfo.attributes\r\n                pledge.name = info.full_name\r\n                pledge.vanityName = info.vanity\r\n            }\r\n        }\r\n\r\n        def pledgeNames = new ArrayList<>()\r\n        def pledgeLog = ''\r\n        List<Pledge> validPledges = new ArrayList<>()\r\n        for (entry in pledges) {\r\n            def currentPledge = entry.value\r\n            validPledges.add(currentPledge)\r\n            pledgeLog += \"- ${currentPledge.getDisplayName()}\\n\"\r\n            pledgeNames.add(currentPledge.getDisplayName())\r\n        }\r\n\r\n        project.ext.patreon.pledges = validPledges\r\n        project.ext.patreon.pledgeNames = pledgeNames.join(', ')\r\n        project.ext.patreon.pledgeLog = pledgeLog\r\n    }\r\n}\r\n\r\nclass Pledge {\r\n\r\n    // The ID for this user in the Patreon system.\r\n    def id\r\n\r\n    // The amount this user is currently paying in USD cents.\r\n    def amountInCents\r\n\r\n    // The date they declined. This will be null if they haven't declined.\r\n    def declined\r\n\r\n    // The full name of the user.\r\n    def name\r\n\r\n    // The vanity name of the user, like a display name.\r\n    def vanityName\r\n\r\n    /*\r\n     Checks if the user is valid, and is paying.\r\n    */\r\n    def isValid() {\r\n        return declined == null && amountInCents > 0\r\n    }\r\n\r\n    /*\r\n     Gets the display name for the user. Defaults to full name if no vanity name is specified by the user.\r\n    */\r\n    def getDisplayName() {\r\n        return vanityName != null ? vanityName : name\r\n    }\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/project_validation.gradle",
    "content": "import javax.imageio.ImageIO\r\nimport java.awt.image.BufferedImage\r\n\r\ngradle.taskGraph.whenReady { graph ->\r\n\r\n    // Validate the logo file for the project.\r\n    // - logo.png must exist in the root project folder.\r\n    // - logo.png must be a 1:1 aspect ratio.\r\n    final File logoFile = project('common').file(\"src/main/resources/logo_${mod_id}.png\")\r\n    if (!logoFile.exists()) {\r\n        throw new GradleException(\"A logo_${mod_id}.png file is required to build this mod.\")\r\n    }\r\n    else {\r\n        try {\r\n            final BufferedImage logoImage = ImageIO.read(logoFile)\r\n            if (logoImage.getWidth() != logoImage.getHeight()) {\r\n                throw new GradleException('The logo image must be a 1:1 aspect ratio.')\r\n            }\r\n        }\r\n        catch (IOException e) {\r\n            throw new GradleException('Unable to process logo file.', e)\r\n        }\r\n    }\r\n\r\n    // Validate the license file for the project.\r\n    // - A file named LICENSE must exist in the root project folder.\r\n    if (!rootProject.file('LICENSE').exists()) {\r\n        throw new GradleException('LICENSE file does not exist.')\r\n    }\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/readme-update.gradle",
    "content": "task updateReadme {\r\n    var readme = rootProject.file('README.md')\r\n    if (!readme.exists()) {\r\n        throw new GradleException('The README.md file is missing!')\r\n    }\r\n    doLast {\r\n        rootProject.logger.lifecycle('Updating the README.md file...')\r\n        var text = readme.text\r\n        text = updateSection(text, 'name', buildName(rootProject))\r\n        text = updateSection(text, 'description', buildIntro(rootProject))\r\n        text = updateSection(text, 'maven', buildMavenInfo(rootProject))\r\n        text = updateSection(text, 'sponsor', buildSponsors(rootProject))\r\n        readme.text = text\r\n    }\r\n}\r\n\r\nstatic String buildName(Project project) {\r\n    var encodedPath = project.group.replaceAll('\\\\.', '%2F')\r\n    var projectPath = project.group.replaceAll('\\\\.', '/')\r\n    var modId = project.property('mod_id')\r\n    var mcVersion = project.property('minecraft_version')\r\n\r\n    var curseBadge = \"[![CurseForge Project](https://img.shields.io/curseforge/dt/${project.property('curse_project')}?logo=curseforge&label=CurseForge&style=flat-square&labelColor=2D2D2D&color=555555)](${project.property('curse_page')})\"\r\n    var modrinthBadge = \"[![Modrinth Project](https://img.shields.io/modrinth/dt/${project.property('modrinth_project')}?logo=modrinth&label=Modrinth&style=flat-square&labelColor=2D2D2D&color=555555)](${project.property('modrinth_page')})\"\r\n    var versionBadge = \"[![Maven Project](https://img.shields.io/maven-metadata/v?style=flat-square&logoColor=D31A38&labelColor=2D2D2D&color=555555&label=Latest&logo=gradle&metadataUrl=https%3A%2F%2Fmaven.blamejared.com%2F${encodedPath}%2F${modId}-common-${mcVersion}%2Fmaven-metadata.xml)](https://maven.blamejared.com/${projectPath})\"\r\n    return \"# ${project.property('mod_name')} ${curseBadge} ${modrinthBadge} ${versionBadge}\"\r\n}\r\n\r\nstatic String buildIntro(Project project) {\r\n    return \"${project.property('mod_description')} The documentation for this mod can be found [here](${project.property('mod_docs')}).\"\r\n}\r\n\r\nstatic String buildMavenInfo(Project project) {\r\n    var group = project.property('group')\r\n    var modId = project.property('mod_id')\r\n    var mcVersion = project.property('minecraft_version')\r\n    var projectVersion = project.version\r\n\r\n    return \"\"\"|## Maven Dependency\r\n    |\r\n    |If you are using [Gradle](https://gradle.org) to manage your dependencies, add the following into your `build.gradle` file. Make sure to replace the version with the correct one. All versions can be viewed [here](https://maven.blamejared.com/${group.replaceAll('\\\\.', '/')}).\r\n    |\r\n    |```gradle\r\n    |repositories {\r\n    |    maven { \r\n    |        url 'https://maven.blamejared.com'\r\n    |    }\r\n    |}\r\n    |\r\n    |dependencies {\r\n    |    // NeoForge\r\n    |    implementation group: '${group}', name: '${modId}-neoforge-${mcVersion}', version: '${projectVersion}'\r\n    |\r\n    |    // Forge\r\n    |    implementation group: '${group}', name: '${modId}-forge-${mcVersion}', version: '${projectVersion}'\r\n    |\r\n    |    // Fabric & Quilt\r\n    |    modImplementation group: '${group}', name: '${modId}-fabric-${mcVersion}', version: '${projectVersion}'\r\n    |\r\n    |    // Common / MultiLoader / Vanilla\r\n    |    compileOnly group: '${group}', name: '${modId}-common-${mcVersion}', version: '${projectVersion}'\r\n    |}\r\n    |```\"\"\".stripMargin()\r\n}\r\n\r\nstatic String buildSponsors(Project project) {\r\n    var modName = project.property('mod_name')\r\n    return \"\"\"|## Sponsors\r\n              |\r\n              |[![](https://assets.blamejared.com/nodecraft/darkhax.jpg)](https://nodecraft.com/r/darkhax)    \r\n              |${modName} is sponsored by Nodecraft. Use code **[DARKHAX](https://nodecraft.com/r/darkhax)** for 30% of your first month of service!\"\"\".stripMargin()\r\n}\r\n\r\nstatic String updateSection(String inputText, String region, String text) {\r\n    var startComment = commentOf(\"${region}-start\")\r\n    var startPos = inputText.indexOf(startComment) + startComment.length()\r\n    var endComment = commentOf(\"${region}-end\")\r\n    var endPos = inputText.indexOf(endComment)\r\n    return inputText.substring(0, startPos) + System.lineSeparator() + text + System.lineSeparator() + inputText.substring(endPos)\r\n}\r\n\r\nstatic String commentOf(String commentText) {\r\n    return \"<!-- ${commentText} -->\"\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/secret-loader.gradle",
    "content": "// Loads properties from a file containing environmental secrets.\r\nimport groovy.json.JsonSlurper\r\n\r\n// Auto detects a secret file and injects it.\r\nif (rootProject.hasProperty('secretFile')) {\r\n    project.logger.lifecycle('Automatically loading properties from the secretFile')\r\n    final def secretsFile = rootProject.file(rootProject.findProperty('secretFile'))\r\n    if (secretsFile.exists() && secretsFile.name.endsWith('.json')) {\r\n        loadProperties(secretsFile)\r\n    }\r\n    else {\r\n        project.logger.lifecycle(\"Properties could not be read from the secretFile because it does not exist. ${secretsFile}\")\r\n    }\r\n}\r\nelse {\r\n    project.logger.lifecycle('The secretFile property has not been set. Some API tokens will not be available.')\r\n}\r\n\r\n// Loads properties using a specified json file.\r\ndef loadProperties(propertyFile) {\r\n    if (propertyFile.exists()) {\r\n        propertyFile.withReader {\r\n            Map propMap = new JsonSlurper().parse it\r\n            for (entry in propMap) {\r\n                // Filter entries that use _comment in the key.\r\n                if (!entry.key.endsWith('_comment')) {\r\n                    project.ext.set(entry.key, entry.value)\r\n                }\r\n            }\r\n            project.logger.lifecycle(\"Successfully loaded ${propMap.size()} environment secrets.\")\r\n            propMap.clear()\r\n        }\r\n    } else {\r\n        project.logger.warn(\"Could not find property file! Expected: ${propertyFile}\")\r\n    }\r\n}"
  },
  {
    "path": "buildSrc/src/main/groovy/version-checker.gradle",
    "content": "// This plugin adds a task that can update the latest version on Jared's\r\n// update checker API. This is a private service that requires an API key\r\n// to use. For more information contact Jared https://x.com/jaredlll08\r\nimport groovy.json.JsonOutput\r\n\r\ntask updateVersionTracker {\r\n\r\n    if (!rootProject.hasProperty('versionTrackerAPI') || !rootProject.hasProperty('versionTrackerUsername')) {\r\n\r\n        rootProject.logger.warn('Skipping Version Checker update. Authentication is required!')\r\n    }\r\n\r\n    onlyIf {\r\n\r\n        rootProject.hasProperty('versionTrackerAPI') && rootProject.hasProperty('versionTrackerUsername')\r\n    }\r\n\r\n    doLast {\r\n\r\n        def username = rootProject.findProperty('versionTrackerUsername')\r\n        def apiKey = rootProject.findProperty('versionTrackerKey')\r\n\r\n        // Creates a Map that acts as the Json body of the API request.\r\n        def body = [\r\n                'author'        : username,\r\n                'projectName'   : project.ext.mod_id,\r\n                'gameVersion'   : project.ext.minecraft_version,\r\n                'projectVersion': project.version,\r\n                'homepage'      : project.ext.curse_page,\r\n                'uid'           : apiKey\r\n        ]\r\n\r\n        // Opens a connection to the version tracker API and writes the payload JSON.\r\n        def req = new URL(rootProject.findProperty('versionTrackerAPI')).openConnection()\r\n        req.setRequestMethod('POST')\r\n        req.setRequestProperty('Content-Type', 'application/json; charset=UTF-8')\r\n        req.setRequestProperty('User-Agent', \"${project.ext.mod_name} Tracker Gradle\")\r\n        req.setDoOutput(true)\r\n        req.getOutputStream().write(JsonOutput.toJson(body).getBytes(\"UTF-8\"))\r\n\r\n        // For the request to be sent we need to read data from the stream.\r\n        project.logger.lifecycle(\"Version Check: Status ${req.getResponseCode()}\")\r\n        project.logger.lifecycle(\"Version Check: Response ${req.getInputStream().getText()}\")\r\n    }\r\n}"
  },
  {
    "path": "common/build.gradle",
    "content": "plugins {\r\n    id 'multiloader-common'\r\n    id 'net.neoforged.moddev'\r\n}\r\n\r\nneoForge {\r\n    neoFormVersion = neo_form_version\r\n    parchment {\r\n        minecraftVersion = parchment_minecraft\r\n        mappingsVersion = parchment_version\r\n    }\r\n}\r\n\r\ndependencies {\r\n    compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5'\r\n    compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.4.0'\r\n    annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.4.0'\r\n\r\n    if (project.hasProperty('jei_version')) {\r\n        compileOnly(\"mezz.jei:jei-${minecraft_version}-common-api:${jei_version}\")\r\n    }\r\n}\r\n\r\nconfigurations {\r\n    commonJava {\r\n        canBeResolved = false\r\n        canBeConsumed = true\r\n    }\r\n    commonResources {\r\n        canBeResolved = false\r\n        canBeConsumed = true\r\n    }\r\n}\r\n\r\nartifacts {\r\n    commonJava sourceSets.main.java.sourceDirectories.singleFile\r\n    commonResources sourceSets.main.resources.sourceDirectories.singleFile\r\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/ModEntry.java",
    "content": "package net.darkhax.bookshelf.common.api;\n\npublic record ModEntry(String modId, String name, String description, String version) {\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/PhysicalSide.java",
    "content": "package net.darkhax.bookshelf.common.api;\n\n/**\n * Represents a physical location in the client/server network diagram.\n */\npublic enum PhysicalSide {\n\n    /**\n     * A physical client. This includes single player, and LAN worlds.\n     */\n    CLIENT,\n\n    /**\n     * A physical server. This includes dedicated servers where client code and logic is not accessible.\n     */\n    SERVER;\n\n    /**\n     * Checks if this is a physical client.\n     *\n     * @return Returns true when on a physical client.\n     */\n    public boolean isClient() {\n\n        return this == CLIENT;\n    }\n\n    /**\n     * Checks if this is a physical server.\n     *\n     * @return Returns true when on a physical server.\n     */\n    public boolean isServer() {\n\n        return this == SERVER;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/annotation/InternalUse.java",
    "content": "package net.darkhax.bookshelf.common.api.annotation;\n\n/**\n * A visual indicator for members that may be visible for technical reasons or convenience but are not intended for\n * general use. For example, if a method you did not create is decorated with this annotation you should not invoke it.\n */\npublic @interface InternalUse {\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/annotation/OnlyFor.java",
    "content": "package net.darkhax.bookshelf.common.api.annotation;\n\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\n\n/**\n * A visual indicator that a class, field, or method can only be accessed in certain environments. There is no special\n * magic or ASM behind this annotation, it is only a visual indicator to help navigate the code.\n */\npublic @interface OnlyFor {\n    PhysicalSide value();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/block/IBlockHooks.java",
    "content": "package net.darkhax.bookshelf.common.api.block;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.world.entity.LightningBolt;\nimport net.minecraft.world.level.BlockGetter;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.pathfinder.PathType;\nimport org.jetbrains.annotations.Nullable;\n\npublic interface IBlockHooks {\n\n    Direction[] LIGHTNING_REDIRECTION_FACES = new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN};\n    Direction[] NO_LIGHTNING_REDIRECTION_FACES = new Direction[]{};\n\n    /**\n     * Allows the block to determine its own pathfinding type.\n     *\n     * @param state   The current state of the block.\n     * @param context Additional context from the world the block is in.\n     * @param pos     The position of the block.\n     * @return The pathfinding type for the block. If null is returned the vanilla behavior for determining pathfinding\n     * will be used instead.\n     */\n    @Nullable\n    default PathType getPathfindingType(BlockState state, BlockGetter context, BlockPos pos) {\n        return null;\n    }\n\n    /**\n     * Called when the block is directly struck by lightning.\n     *\n     * @param state     The state of this block.\n     * @param level     The level.\n     * @param pos       The position of this block.\n     * @param lightning The lightning bolt that hit the block.\n     */\n    default void onLightningStrike(BlockState state, Level level, BlockPos pos, LightningBolt lightning) {\n    }\n\n    /**\n     * Called when a neighbor is struck by lightning and the block is not insulated from the strike.\n     *\n     * @param state        The state of this block.\n     * @param level        The level.\n     * @param pos          The position of this block.\n     * @param lightning    The lightning bolt that hit the block.\n     * @param strikeOrigin The original strike position of the lightning bolt.\n     */\n    default void onLightningStrikeIndirect(BlockState state, Level level, BlockPos pos, LightningBolt lightning, BlockPos strikeOrigin) {\n    }\n\n    /**\n     * Provides an array of directions lightning can travel and indirectly hit when this block is hit by lightning.\n     *\n     * @param state The state of this block.\n     * @param level The level.\n     * @param pos   The position of this block.\n     * @return An array of directions that should be indirectly hit by the lightning.\n     */\n    default Direction[] redirectLightningStrike(BlockState state, Level level, BlockPos pos) {\n        return NO_LIGHTNING_REDIRECTION_FACES;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/IEnumCommand.java",
    "content": "package net.darkhax.bookshelf.common.api.commands;\n\nimport com.mojang.brigadier.Command;\nimport net.minecraft.commands.CommandSourceStack;\n\n/**\n * Allows an enum to be used as a branching command path.\n */\npublic interface IEnumCommand extends Command<CommandSourceStack> {\n\n    /**\n     * Gets the name of the command. This must be unique for each enum value.\n     *\n     * @return The name of the command.\n     */\n    String getCommandName();\n\n    /**\n     * Gets the required permission level to perform the command.\n     *\n     * @return The required permission level.\n     */\n    default PermissionLevel requiredPermissionLevel() {\n        return PermissionLevel.PLAYER;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/PermissionLevel.java",
    "content": "package net.darkhax.bookshelf.common.api.commands;\n\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\n\nimport java.util.function.Predicate;\n\npublic enum PermissionLevel implements Predicate<CommandSourceStack> {\n\n    /**\n     * All players will generally meet the requirements for this permission level.\n     */\n    PLAYER(Commands.LEVEL_ALL),\n\n    /**\n     * These players have slightly elevated permission levels. In vanilla, they do not gain access to any additional\n     * commands, but they are able to bypass spawn chunk protection.\n     */\n    MODERATOR(Commands.LEVEL_MODERATORS),\n\n    /**\n     * These players can execute commands that modify the world and player data. They are also allowed to use and modify\n     * command blocks.\n     */\n    GAMEMASTER(Commands.LEVEL_GAMEMASTERS),\n\n    /**\n     * These players can use commands related to player management. For example, they can ban, kick, op, and de-op.\n     */\n    ADMIN(Commands.LEVEL_ADMINS),\n\n    /**\n     * This is the highest permission level available in vanilla Minecraft. Players with this permission level generally\n     * have no restrictions.\n     */\n    OWNER(Commands.LEVEL_OWNERS);\n\n    final int level;\n\n    PermissionLevel(int level) {\n        this.level = level;\n    }\n\n    public int get() {\n        return this.level;\n    }\n\n    @Override\n    public boolean test(CommandSourceStack source) {\n        return source.hasPermission(this.level);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/ArgumentSerializer.java",
    "content": "package net.darkhax.bookshelf.common.api.commands.args;\n\nimport com.google.gson.JsonObject;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport com.mojang.serialization.JsonOps;\nimport com.mojang.serialization.MapCodec;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ArgumentSerializer<T extends ArgumentType<?>, V> implements ArgumentTypeInfo<T, ArgumentSerializer.ArgTemplate<T, V>> {\n\n    private final MapCodec<V> codec;\n    private final StreamCodec<FriendlyByteBuf, V> stream;\n    private final BiFunction<CommandBuildContext, V, T> fromData;\n    private final Function<T, V> toData;\n\n    public ArgumentSerializer(MapCodec<V> codec, StreamCodec<FriendlyByteBuf, V> stream, BiFunction<CommandBuildContext, V, T> mapFunc, Function<T, V> toData) {\n        this.codec = codec;\n        this.stream = stream;\n        this.fromData = mapFunc;\n        this.toData = toData;\n    }\n\n    @Override\n    public void serializeToNetwork(ArgTemplate<T, V> template, @NotNull FriendlyByteBuf buf) {\n        this.stream.encode(buf, template.data);\n    }\n\n    @NotNull\n    @Override\n    public ArgTemplate<T, V> deserializeFromNetwork(@NotNull FriendlyByteBuf buf) {\n        return new ArgTemplate<>(this, this.stream.decode(buf));\n    }\n\n    @Override\n    public void serializeToJson(@NotNull ArgTemplate<T, V> template, @NotNull JsonObject json) {\n        json.add(\"value\", this.codec.codec().encodeStart(JsonOps.INSTANCE, template.data).getOrThrow());\n    }\n\n    @NotNull\n    @Override\n    public ArgTemplate<T, V> unpack(@NotNull T t) {\n        return new ArgTemplate<>(this, this.toData.apply(t));\n    }\n\n    public static class ArgTemplate<T extends ArgumentType<?>, V> implements ArgumentTypeInfo.Template<T> {\n\n        private final ArgumentSerializer<T, V> type;\n        private final V data;\n\n        protected ArgTemplate(ArgumentSerializer<T, V> type, V data) {\n            this.type = type;\n            this.data = data;\n        }\n\n        @NotNull\n        @Override\n        public T instantiate(@NotNull CommandBuildContext ctx) {\n            return this.type.fromData.apply(ctx, this.data);\n        }\n\n        @NotNull\n        @Override\n        public ArgumentTypeInfo<T, ?> type() {\n            return this.type;\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/FontArgument.java",
    "content": "package net.darkhax.bookshelf.common.api.commands.args;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport com.mojang.brigadier.builder.RequiredArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport com.mojang.brigadier.suggestion.Suggestions;\nimport com.mojang.brigadier.suggestion.SuggestionsBuilder;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.api.text.font.BuiltinFonts;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.commands.SharedSuggestionProvider;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.Collection;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\n\npublic class FontArgument implements ArgumentType<ResourceLocation> {\n\n    public static final FontArgument ARGUMENT = new FontArgument();\n    public static final ArgumentTypeInfo<FontArgument, ?> SERIALIZER = SingletonArgumentInfo.of(() -> ARGUMENT);\n    private static final Set<String> EXAMPLES = Set.of(BuiltinFonts.DEFAULT.identifier().toString(), BuiltinFonts.ALT.identifier().toString(), BuiltinFonts.ILLAGER.identifier().toString());\n\n    public static ResourceLocation get(CommandContext<CommandSourceStack> context) {\n        return get(\"font\", context);\n    }\n\n    public static ResourceLocation get(String argName, CommandContext<CommandSourceStack> context) {\n        return context.getArgument(argName, ResourceLocation.class);\n    }\n\n    public static RequiredArgumentBuilder<CommandSourceStack, ResourceLocation> argument() {\n        return argument(\"font\");\n    }\n\n    public static RequiredArgumentBuilder<CommandSourceStack, ResourceLocation> argument(String argName) {\n        return Commands.argument(argName, ARGUMENT);\n    }\n\n    @Override\n    public ResourceLocation parse(StringReader reader) throws CommandSyntaxException {\n        return ResourceLocation.read(reader);\n    }\n\n    @Override\n    public Collection<String> getExamples() {\n        return EXAMPLES;\n    }\n\n    @Override\n    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {\n        if (Services.PLATFORM.isPhysicalClient()) {\n            return SharedSuggestionProvider.suggestResource(TextHelper.getRegisteredFonts(), builder);\n        }\n        return SharedSuggestionProvider.suggestResource(BuiltinFonts.FONT_IDS, builder);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/SingletonArgumentInfo.java",
    "content": "package net.darkhax.bookshelf.common.api.commands.args;\n\nimport com.google.gson.JsonObject;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.network.FriendlyByteBuf;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\n\n/**\n * An argument info type that will always resolve to the same singleton instance. This is beneficial when the values are\n * already known by the client in advance, or may even be only known by the client.\n *\n * @param <T> The argument type.\n */\npublic final class SingletonArgumentInfo<T extends ArgumentType<?>> implements ArgumentTypeInfo<T, SingletonArgumentInfo.Template<T>> {\n\n    /**\n     * Creates argument info for a given argument instance.\n     *\n     * @param argSupplier A supplier that resolves the singleton instance. This supplier should always return the same\n     *                    instance!\n     * @param <T>         The argument type.\n     * @return The argument info for the singleton.\n     */\n    public static <T extends ArgumentType<?>> SingletonArgumentInfo<T> of(Supplier<T> argSupplier) {\n        return new SingletonArgumentInfo<>(argSupplier);\n    }\n\n    private final CachedSupplier<Template<T>> templateSupplier;\n\n    private SingletonArgumentInfo(Supplier<T> singletonSupplier) {\n        this.templateSupplier = CachedSupplier.cache(() -> new Template<>(singletonSupplier, this));\n    }\n\n    @Override\n    public void serializeToNetwork(@NotNull Template<T> tTemplate, @NotNull FriendlyByteBuf friendlyByteBuf) {\n        // NO-OP\n    }\n\n    @NotNull\n    @Override\n    public Template<T> deserializeFromNetwork(@NotNull FriendlyByteBuf buffer) {\n        return this.templateSupplier.get();\n    }\n\n    @Override\n    public void serializeToJson(@NotNull Template<T> tTemplate, @NotNull JsonObject jsonObject) {\n        // NO-OP\n    }\n\n    @NotNull\n    @Override\n    public Template<T> unpack(@NotNull T template) {\n        return this.templateSupplier.get();\n    }\n\n    /**\n     * A template that holds a cached argument singleton.\n     *\n     * @param <T> The argument type.\n     */\n    public static class Template<T extends ArgumentType<?>> implements ArgumentTypeInfo.Template<T> {\n\n        private final ArgumentTypeInfo<T, ?> info;\n        private final Supplier<T> singletonSupplier;\n\n        protected Template(Supplier<T> supplier, ArgumentTypeInfo<T, ?> info) {\n            this.singletonSupplier = supplier;\n            this.info = info;\n        }\n\n        @NotNull\n        @Override\n        public T instantiate(@NotNull CommandBuildContext ctx) {\n            return this.singletonSupplier.get();\n        }\n\n        @NotNull\n        @Override\n        public ArgumentTypeInfo<T, ?> type() {\n            return this.info;\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/TagArgument.java",
    "content": "package net.darkhax.bookshelf.common.api.commands.args;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport com.mojang.brigadier.suggestion.Suggestions;\nimport com.mojang.brigadier.suggestion.SuggestionsBuilder;\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.SharedSuggestionProvider;\nimport net.minecraft.core.HolderLookup;\nimport net.minecraft.core.Registry;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.tags.TagKey;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.concurrent.CompletableFuture;\n\npublic class TagArgument<T> implements ArgumentType<TagKey<T>> {\n\n    private static final MapCodec<ResourceKey<? extends Registry<?>>> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(\n            ResourceLocation.CODEC.fieldOf(\"registry_name\").forGetter(ResourceKey::location)\n    ).apply(instance, ResourceKey::createRegistryKey));\n    private static final StreamCodec<FriendlyByteBuf, ResourceKey<? extends Registry<?>>> STREAM = StreamCodec.of(\n            (buf, key) -> buf.writeResourceLocation(key.location()),\n            buf -> ResourceKey.createRegistryKey(buf.readResourceLocation())\n    );\n    public static final ArgumentSerializer<TagArgument<?>, ResourceKey<? extends Registry<?>>> SERIALIZER = new ArgumentSerializer<>(CODEC, STREAM, TagArgument::makeRaw, t -> t.registryKey);\n    private static final Collection<String> EXAMPLES = Arrays.asList(\"minecraft:dirt\", \"minecraft:axolotl_food\", \"minecraft:enchantable/bow\");\n\n    private final HolderLookup<T> registryLookup;\n    private final ResourceKey<? extends Registry<T>> registryKey;\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static TagArgument<?> makeRaw(CommandBuildContext context, ResourceKey registryKey) {\n        return new TagArgument<>(context, registryKey);\n    }\n\n    private TagArgument(CommandBuildContext context, ResourceKey<? extends Registry<T>> registryKey) {\n        this.registryKey = registryKey;\n        this.registryLookup = context.lookupOrThrow(registryKey);\n    }\n\n    @Override\n    public TagKey<T> parse(StringReader reader) throws CommandSyntaxException {\n        final ResourceLocation tagId = ResourceLocation.read(reader);\n        return TagKey.create(this.registryKey, tagId);\n    }\n\n    @Override\n    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {\n        return SharedSuggestionProvider.suggestResource(this.registryLookup.listTagIds().map(TagKey::location), builder);\n    }\n\n    @Override\n    public Collection<String> getExamples() {\n        return EXAMPLES;\n    }\n\n    public static <T> TagArgument<T> arg(CommandBuildContext context, ResourceKey<? extends Registry<T>> registry) {\n        return new TagArgument<>(context, registry);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> TagKey<T> get(String argName, CommandContext<CommandSourceStack> context, ResourceKey<? extends Registry<T>> registry) {\n        return (TagKey<T>) context.getArgument(argName, TagKey.class);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/BookshelfTags.java",
    "content": "package net.darkhax.bookshelf.common.api.data;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.damagesource.DamageType;\n\npublic class BookshelfTags {\n    public static TagKey<DamageType> FAKE_PLAYER_DAMAGE = TagKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, \"fake_player\"));\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/ISidedRecipeManager.java",
    "content": "package net.darkhax.bookshelf.common.api.data;\n\npublic interface ISidedRecipeManager {\n\n    void bookshelf$setLogicalClient();\n\n    void bookshelf$setLogicalServer();\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/EnumStreamCodec.java",
    "content": "package net.darkhax.bookshelf.common.api.data.codecs;\n\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport org.jetbrains.annotations.NotNull;\n\npublic class EnumStreamCodec<T extends Enum<T>> implements StreamCodec<FriendlyByteBuf, T> {\n\n\tprivate final Class<T> enumClass;\n\n\tpublic EnumStreamCodec(Class<T> clazz) {\n\t\tthis.enumClass = clazz;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic T decode(FriendlyByteBuf buf) {\n\t\treturn buf.readEnum(enumClass);\n\t}\n\n\t@Override\n\tpublic void encode(FriendlyByteBuf buf, @NotNull T toWrite) {\n\t\tbuf.writeEnum(toWrite);\n\t}\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/MapCodecHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.data.codecs.map;\n\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.minecraft.util.random.SimpleWeightedRandomList;\nimport net.minecraft.util.random.WeightedEntry;\n\nimport java.lang.reflect.Array;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.function.IntFunction;\n\n/**\n * A CodecHelper wraps a Codec to provide a large amount of helpers and utilities to make working with the Codec easier\n * and more flexible.\n *\n * @param <T> The type handled by the codec helper.\n */\npublic class MapCodecHelper<T> {\n\n    /**\n     * The root codec that powers all the helpers and utilities offered by the CodecHelper.\n     */\n    private final Codec<T> elementCodec;\n\n    /**\n     * A function for creating an array of the codecs type. This allows us to create clean arrays for our codecs without\n     * relying on sketchy code.\n     */\n    private final IntFunction<T[]> arrayBuilder;\n\n    @SafeVarargs\n    public MapCodecHelper(Codec<T> elementCodec, T... vargs) {\n\n        if (vargs.length > 0) {\n            throw new IllegalArgumentException(\"The arrayBuilder must be empty!\");\n        }\n\n        this.elementCodec = elementCodec;\n        this.arrayBuilder = size -> (T[]) Array.newInstance(vargs.getClass().getComponentType(), size);\n    }\n\n    /**\n     * Gets a codec that can read and write single instances of the element.\n     *\n     * @return A Codec that can read and write single instances of the element.\n     */\n    public Codec<T> get() {\n\n        return this.elementCodec;\n    }\n\n    /**\n     * A helper for defining a field of this type in a RecordCodecBuilder.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodecBuilder.\n     * @return A RecordCodecBuilder that represents a field.\n     */\n    public <O> RecordCodecBuilder<O, T> get(String fieldName, Function<O, T> getter) {\n\n        return this.get().fieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field of this type in a RecordCodecBuilder. If the field is not present the fallback\n     * value will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodecBuilder.\n     * @return A RecordCodecBuilder that represents a field of the helpers type with a fallback value.\n     */\n    public <O> RecordCodecBuilder<O, T> get(String fieldName, Function<O, T> getter, T fallback) {\n\n        return this.get().optionalFieldOf(fieldName, fallback).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write an array. For the sake of convenience single elements are treated as an\n     * array of one.\n     *\n     * @return A Codec that can read and write an array.\n     */\n    public Codec<T[]> getArray() {\n\n        return MapCodecs.flexibleArray(this.get(), this.arrayBuilder);\n    }\n\n    /**\n     * A helper for defining a field for an array of this type in a RecordCodecBuilder. For the sake of convenience\n     * single elements are treated as an array of one.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for an array.\n     */\n    public <O> RecordCodecBuilder<O, T[]> getArray(String fieldName, Function<O, T[]> getter) {\n\n        return this.getArray().fieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for an array of this type in a RecordCodecBuilder. For the sake of convenience\n     * single elements are treated as an array of one. If the field is not present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for an array.\n     */\n    public <O> RecordCodecBuilder<O, T[]> getArray(String fieldName, Function<O, T[]> getter, T... fallback) {\n\n        return this.getArray().optionalFieldOf(fieldName, fallback).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write a list. For the sake of convenience single elements are treated as a list of\n     * one.\n     *\n     * @return A Codec that can read and write a list.\n     */\n    public Codec<List<T>> getList() {\n\n        return MapCodecs.flexibleList(this.get());\n    }\n\n    /**\n     * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a list of one.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a list.\n     */\n    public <O> RecordCodecBuilder<O, List<T>> getList(String fieldName, Function<O, List<T>> getter) {\n\n        return this.getList().fieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a list of one. If the field is not present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a list.\n     */\n    public <O> RecordCodecBuilder<O, List<T>> getList(String fieldName, Function<O, List<T>> getter, List<T> fallback) {\n\n        return this.getList().optionalFieldOf(fieldName, fallback).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a list of one. If the field is not present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a list.\n     */\n    public <O> RecordCodecBuilder<O, List<T>> getList(String fieldName, Function<O, List<T>> getter, T... fallback) {\n\n        return this.getList().optionalFieldOf(fieldName, List.of(fallback)).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write a set. For the sake of convenience single elements are treated as a list of\n     * one.\n     *\n     * @return A Codec that can read and write a set.\n     */\n    public Codec<Set<T>> getSet() {\n\n        return MapCodecs.flexibleSet(this.get());\n    }\n\n    /**\n     * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a set of one.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a set.\n     */\n    public <O> RecordCodecBuilder<O, Set<T>> getSet(String fieldName, Function<O, Set<T>> getter) {\n\n        return this.getSet().fieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a set of one. If the field is not present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a set.\n     */\n    public <O> RecordCodecBuilder<O, Set<T>> getSet(String fieldName, Function<O, Set<T>> getter, Set<T> fallback) {\n\n        return this.getSet().optionalFieldOf(fieldName, fallback).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single\n     * elements are treated as a set of one. If the field is not present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a set.\n     */\n    public <O> RecordCodecBuilder<O, Set<T>> getSet(String fieldName, Function<O, Set<T>> getter, T... fallback) {\n\n        return this.getSet().optionalFieldOf(fieldName, Set.of(fallback)).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write an optional value.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @return A Codec that can read and write an optional value.\n     */\n    public MapCodec<Optional<T>> getOptional(String fieldName) {\n\n        return this.get().optionalFieldOf(fieldName);\n    }\n\n    /**\n     * A helper for defining a field for an optional value of this type in a RecordCodecBuilder.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for an optional value.\n     */\n    public <O> RecordCodecBuilder<O, Optional<T>> getOptional(String fieldName, Function<O, Optional<T>> getter) {\n\n        return this.get().optionalFieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * A helper for defining a field for an optional value of this type in a RecordCodecBuilder. If the field is not\n     * present the fallback will be used.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param fallback  The fallback value to use when the field is not present.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for an optional value.\n     */\n    public <O> RecordCodecBuilder<O, Optional<T>> getOptional(String fieldName, Function<O, Optional<T>> getter, Optional<T> fallback) {\n\n        return MapCodecs.optional(this.get(), fieldName, fallback, true).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write nullable values.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @return A Codec that can read and write nullable values.\n     */\n    public MapCodec<T> getNullable(String fieldName) {\n\n        return MapCodecs.nullable(this.get(), fieldName);\n    }\n\n    /**\n     * A helper for defining a field for a nullable value of this type in a RecordCodecBuilder.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodedBuilder.\n     * @return A RecordCodecBuilder that represents a field for a nullable value.\n     */\n    public <O> RecordCodecBuilder<O, T> getNullable(String fieldName, Function<O, T> getter) {\n\n        return this.getNullable(fieldName).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write a weighted entry.\n     *\n     * @return A Codec that can read and write a weighted entry.\n     */\n    public Codec<WeightedEntry.Wrapper<T>> getWeighted() {\n\n        return WeightedEntry.Wrapper.codec(this.get());\n    }\n\n    /**\n     * A helper for defining a field for a weighted entry of this type in a RecordCodecBuilder.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodecBuilder.\n     * @return A RecordCodecBuilder that represents a field for a weighted entry.\n     */\n    public <O> RecordCodecBuilder<O, WeightedEntry.Wrapper<T>> getWeighted(String fieldName, Function<O, WeightedEntry.Wrapper<T>> getter) {\n\n        return WeightedEntry.Wrapper.codec(this.get()).fieldOf(fieldName).forGetter(getter);\n    }\n\n    /**\n     * Gets a codec that can read and write a weighted list.\n     *\n     * @return A Codec that can read and write a weighted list.\n     */\n    public Codec<SimpleWeightedRandomList<T>> getWeightedList() {\n\n        return SimpleWeightedRandomList.wrappedCodec(this.get());\n    }\n\n    /**\n     * A helper for defining a field for a weighted list of this type in a RecordCodecBuilder.\n     *\n     * @param fieldName The name of the field to read the value from.\n     * @param getter    A getter that will read the value from an object of the RecordCodecBuilders type.\n     * @param <O>       The type of the RecordCodecBuilder.\n     * @return A RecordCodecBuilder that represents a field for a weighted list.\n     */\n    public <O> RecordCodecBuilder<O, SimpleWeightedRandomList<T>> getWeightedList(String fieldName, Function<O, SimpleWeightedRandomList<T>> getter) {\n\n        return this.getWeightedList().fieldOf(fieldName).forGetter(getter);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/MapCodecs.java",
    "content": "package net.darkhax.bookshelf.common.api.data.codecs.map;\n\nimport com.mojang.datafixers.util.Either;\nimport com.mojang.datafixers.util.Pair;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.util.FunctionHelper;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.Optionull;\nimport net.minecraft.advancements.CriterionTrigger;\nimport net.minecraft.advancements.critereon.EntitySubPredicate;\nimport net.minecraft.advancements.critereon.ItemSubPredicate;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.UUIDUtil;\nimport net.minecraft.core.component.DataComponentType;\nimport net.minecraft.core.particles.ParticleType;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.ComponentSerialization;\nimport net.minecraft.network.chat.numbers.NumberFormatType;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.sounds.SoundEvent;\nimport net.minecraft.sounds.SoundSource;\nimport net.minecraft.stats.StatType;\nimport net.minecraft.util.ExtraCodecs;\nimport net.minecraft.util.valueproviders.FloatProviderType;\nimport net.minecraft.util.valueproviders.IntProviderType;\nimport net.minecraft.world.Difficulty;\nimport net.minecraft.world.effect.MobEffect;\nimport net.minecraft.world.effect.MobEffectInstance;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.EquipmentSlot;\nimport net.minecraft.world.entity.MobCategory;\nimport net.minecraft.world.entity.ai.attributes.Attribute;\nimport net.minecraft.world.entity.ai.attributes.AttributeModifier;\nimport net.minecraft.world.entity.ai.memory.MemoryModuleType;\nimport net.minecraft.world.entity.ai.sensing.SensorType;\nimport net.minecraft.world.entity.ai.village.poi.PoiType;\nimport net.minecraft.world.entity.animal.CatVariant;\nimport net.minecraft.world.entity.animal.FrogVariant;\nimport net.minecraft.world.entity.npc.VillagerProfession;\nimport net.minecraft.world.entity.npc.VillagerType;\nimport net.minecraft.world.entity.schedule.Activity;\nimport net.minecraft.world.entity.schedule.Schedule;\nimport net.minecraft.world.inventory.MenuType;\nimport net.minecraft.world.item.ArmorMaterial;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.DyeColor;\nimport net.minecraft.world.item.Instrument;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Rarity;\nimport net.minecraft.world.item.alchemy.Potion;\nimport net.minecraft.world.item.crafting.Ingredient;\nimport net.minecraft.world.item.crafting.RecipeSerializer;\nimport net.minecraft.world.item.crafting.RecipeType;\nimport net.minecraft.world.item.enchantment.LevelBasedValue;\nimport net.minecraft.world.item.enchantment.effects.EnchantmentEntityEffect;\nimport net.minecraft.world.item.enchantment.effects.EnchantmentLocationBasedEffect;\nimport net.minecraft.world.item.enchantment.effects.EnchantmentValueEffect;\nimport net.minecraft.world.item.enchantment.providers.EnchantmentProvider;\nimport net.minecraft.world.level.biome.BiomeSource;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.Mirror;\nimport net.minecraft.world.level.block.Rotation;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.world.level.block.entity.DecoratedPotPattern;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.block.state.StateDefinition;\nimport net.minecraft.world.level.block.state.properties.Property;\nimport net.minecraft.world.level.chunk.ChunkGenerator;\nimport net.minecraft.world.level.chunk.status.ChunkStatus;\nimport net.minecraft.world.level.gameevent.GameEvent;\nimport net.minecraft.world.level.gameevent.PositionSourceType;\nimport net.minecraft.world.level.levelgen.DensityFunction;\nimport net.minecraft.world.level.levelgen.SurfaceRules;\nimport net.minecraft.world.level.levelgen.blockpredicates.BlockPredicateType;\nimport net.minecraft.world.level.levelgen.carver.WorldCarver;\nimport net.minecraft.world.level.levelgen.feature.Feature;\nimport net.minecraft.world.level.levelgen.feature.featuresize.FeatureSizeType;\nimport net.minecraft.world.level.levelgen.feature.foliageplacers.FoliagePlacerType;\nimport net.minecraft.world.level.levelgen.feature.rootplacers.RootPlacerType;\nimport net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProviderType;\nimport net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecoratorType;\nimport net.minecraft.world.level.levelgen.feature.trunkplacers.TrunkPlacerType;\nimport net.minecraft.world.level.levelgen.heightproviders.HeightProviderType;\nimport net.minecraft.world.level.levelgen.placement.PlacementModifierType;\nimport net.minecraft.world.level.levelgen.structure.StructureType;\nimport net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;\nimport net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;\nimport net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;\nimport net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;\nimport net.minecraft.world.level.levelgen.structure.templatesystem.PosRuleTestType;\nimport net.minecraft.world.level.levelgen.structure.templatesystem.RuleTestType;\nimport net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType;\nimport net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifierType;\nimport net.minecraft.world.level.material.Fluid;\nimport net.minecraft.world.level.saveddata.maps.MapDecorationType;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunctionType;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;\nimport net.minecraft.world.level.storage.loot.providers.nbt.LootNbtProviderType;\nimport net.minecraft.world.level.storage.loot.providers.number.LootNumberProviderType;\nimport net.minecraft.world.level.storage.loot.providers.score.LootScoreProviderType;\nimport org.joml.Vector3f;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.StringJoiner;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.function.IntFunction;\nimport java.util.function.Supplier;\nimport java.util.function.UnaryOperator;\nimport java.util.stream.Collectors;\n\n@SuppressWarnings(\"unused\")\npublic class MapCodecs {\n\n    // JAVA TYPES\n    public static final MapCodecHelper<Boolean> BOOLEAN = new MapCodecHelper<>(Codec.BOOL);\n    public static final MapCodecHelper<Byte> BYTE = new MapCodecHelper<>(Codec.BYTE);\n    public static final MapCodecHelper<Short> SHORT = new MapCodecHelper<>(Codec.SHORT);\n    public static final MapCodecHelper<Integer> INT = new MapCodecHelper<>(Codec.INT);\n    public static final MapCodecHelper<Float> FLOAT = new MapCodecHelper<>(Codec.FLOAT);\n    public static final MapCodecHelper<Long> LONG = new MapCodecHelper<>(Codec.LONG);\n    public static final MapCodecHelper<Double> DOUBLE = new MapCodecHelper<>(Codec.DOUBLE);\n    public static final MapCodecHelper<String> STRING = new MapCodecHelper<>(Codec.STRING);\n    public static final MapCodecHelper<UUID> UUID = new MapCodecHelper<>(UUIDUtil.CODEC);\n\n    // REGISTRIES\n    public static final MapCodecHelper<Holder<GameEvent>> GAME_EVENT = RegistryMapCodecHelper.create(BuiltInRegistries.GAME_EVENT);\n    public static final MapCodecHelper<Holder<SoundEvent>> SOUND_EVENT = RegistryMapCodecHelper.create(BuiltInRegistries.SOUND_EVENT);\n    public static final MapCodecHelper<Holder<Fluid>> FLUID = RegistryMapCodecHelper.create(BuiltInRegistries.FLUID);\n    public static final MapCodecHelper<Holder<MobEffect>> MOB_EFFECT = RegistryMapCodecHelper.create(BuiltInRegistries.MOB_EFFECT);\n    public static final MapCodecHelper<Holder<Block>> BLOCK = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK);\n    public static final MapCodecHelper<Holder<EntityType<?>>> ENTITY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENTITY_TYPE);\n    public static final MapCodecHelper<Holder<Item>> ITEM = RegistryMapCodecHelper.create(BuiltInRegistries.ITEM);\n    public static final MapCodecHelper<Holder<Potion>> POTION = RegistryMapCodecHelper.create(BuiltInRegistries.POTION);\n    public static final MapCodecHelper<Holder<ParticleType<?>>> PARTICLE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.PARTICLE_TYPE);\n    public static final MapCodecHelper<Holder<BlockEntityType<?>>> BLOCK_ENTITY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_ENTITY_TYPE);\n    public static final MapCodecHelper<Holder<ResourceLocation>> CUSTOM_STAT = RegistryMapCodecHelper.create(BuiltInRegistries.CUSTOM_STAT);\n    public static final MapCodecHelper<Holder<ChunkStatus>> CHUNK_STATUS = RegistryMapCodecHelper.create(BuiltInRegistries.CHUNK_STATUS);\n    public static final MapCodecHelper<Holder<RuleTestType<?>>> RULE_TEST = RegistryMapCodecHelper.create(BuiltInRegistries.RULE_TEST);\n    public static final MapCodecHelper<Holder<RuleBlockEntityModifierType<?>>> RULE_BLOCK_ENTITY_MODIFIER = RegistryMapCodecHelper.create(BuiltInRegistries.RULE_BLOCK_ENTITY_MODIFIER);\n    public static final MapCodecHelper<Holder<PosRuleTestType<?>>> POS_RULE_TEST = RegistryMapCodecHelper.create(BuiltInRegistries.POS_RULE_TEST);\n    public static final MapCodecHelper<Holder<MenuType<?>>> MENU = RegistryMapCodecHelper.create(BuiltInRegistries.MENU);\n    public static final MapCodecHelper<Holder<RecipeType<?>>> RECIPE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.RECIPE_TYPE);\n    public static final MapCodecHelper<Holder<RecipeSerializer<?>>> RECIPE_SERIALIZER = RegistryMapCodecHelper.create(BuiltInRegistries.RECIPE_SERIALIZER);\n    public static final MapCodecHelper<Holder<Attribute>> ATTRIBUTE = RegistryMapCodecHelper.create(BuiltInRegistries.ATTRIBUTE);\n    public static final MapCodecHelper<Holder<PositionSourceType<?>>> POSITION_SOURCE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POSITION_SOURCE_TYPE);\n    public static final MapCodecHelper<Holder<ArgumentTypeInfo<?, ?>>> COMMAND_ARGUMENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.COMMAND_ARGUMENT_TYPE);\n    public static final MapCodecHelper<Holder<StatType<?>>> STAT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.STAT_TYPE);\n    public static final MapCodecHelper<Holder<VillagerType>> VILLAGER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.VILLAGER_TYPE);\n    public static final MapCodecHelper<Holder<VillagerProfession>> VILLAGER_PROFESSION = RegistryMapCodecHelper.create(BuiltInRegistries.VILLAGER_PROFESSION);\n    public static final MapCodecHelper<Holder<PoiType>> POINT_OF_INTEREST_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POINT_OF_INTEREST_TYPE);\n    public static final MapCodecHelper<Holder<MemoryModuleType<?>>> MEMORY_MODULE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.MEMORY_MODULE_TYPE);\n    public static final MapCodecHelper<Holder<SensorType<?>>> SENSOR_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.SENSOR_TYPE);\n    public static final MapCodecHelper<Holder<Schedule>> SCHEDULE = RegistryMapCodecHelper.create(BuiltInRegistries.SCHEDULE);\n    public static final MapCodecHelper<Holder<Activity>> ACTIVITY = RegistryMapCodecHelper.create(BuiltInRegistries.ACTIVITY);\n    public static final MapCodecHelper<Holder<LootPoolEntryType>> LOOT_POOL_ENTRY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE);\n    public static final MapCodecHelper<Holder<LootItemFunctionType<?>>> LOOT_FUNCTION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_FUNCTION_TYPE);\n    public static final MapCodecHelper<Holder<LootItemConditionType>> LOOT_CONDITION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_CONDITION_TYPE);\n    public static final MapCodecHelper<Holder<LootNumberProviderType>> LOOT_NUMBER_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_NUMBER_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<LootNbtProviderType>> LOOT_NBT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_NBT_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<LootScoreProviderType>> LOOT_SCORE_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_SCORE_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<FloatProviderType<?>>> FLOAT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FLOAT_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<IntProviderType<?>>> INT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.INT_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<HeightProviderType<?>>> HEIGHT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.HEIGHT_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<BlockPredicateType<?>>> BLOCK_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_PREDICATE_TYPE);\n    public static final MapCodecHelper<Holder<WorldCarver<?>>> CARVER = RegistryMapCodecHelper.create(BuiltInRegistries.CARVER);\n    public static final MapCodecHelper<Holder<Feature<?>>> FEATURE = RegistryMapCodecHelper.create(BuiltInRegistries.FEATURE);\n    public static final MapCodecHelper<Holder<StructurePlacementType<?>>> STRUCTURE_PLACEMENT = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PLACEMENT);\n    public static final MapCodecHelper<Holder<StructurePieceType>> STRUCTURE_PIECE = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PIECE);\n    public static final MapCodecHelper<Holder<StructureType<?>>> STRUCTURE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_TYPE);\n    public static final MapCodecHelper<Holder<PlacementModifierType<?>>> PLACEMENT_MODIFIER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.PLACEMENT_MODIFIER_TYPE);\n    public static final MapCodecHelper<Holder<BlockStateProviderType<?>>> BLOCKSTATE_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCKSTATE_PROVIDER_TYPE);\n    public static final MapCodecHelper<Holder<FoliagePlacerType<?>>> FOLIAGE_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FOLIAGE_PLACER_TYPE);\n    public static final MapCodecHelper<Holder<TrunkPlacerType<?>>> TRUNK_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.TRUNK_PLACER_TYPE);\n    public static final MapCodecHelper<Holder<RootPlacerType<?>>> ROOT_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ROOT_PLACER_TYPE);\n    public static final MapCodecHelper<Holder<TreeDecoratorType<?>>> TREE_DECORATOR_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.TREE_DECORATOR_TYPE);\n    public static final MapCodecHelper<Holder<FeatureSizeType<?>>> FEATURE_SIZE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FEATURE_SIZE_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends BiomeSource>>> BIOME_SOURCE = RegistryMapCodecHelper.create(BuiltInRegistries.BIOME_SOURCE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends ChunkGenerator>>> CHUNK_GENERATOR = RegistryMapCodecHelper.create(BuiltInRegistries.CHUNK_GENERATOR);\n    public static final MapCodecHelper<Holder<MapCodec<? extends SurfaceRules.ConditionSource>>> MATERIAL_CONDITION = RegistryMapCodecHelper.create(BuiltInRegistries.MATERIAL_CONDITION);\n    public static final MapCodecHelper<Holder<MapCodec<? extends SurfaceRules.RuleSource>>> MATERIAL_RULE = RegistryMapCodecHelper.create(BuiltInRegistries.MATERIAL_RULE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends DensityFunction>>> DENSITY_FUNCTION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.DENSITY_FUNCTION_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends Block>>> BLOCK_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_TYPE);\n    public static final MapCodecHelper<Holder<StructureProcessorType<?>>> STRUCTURE_PROCESSOR = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PROCESSOR);\n    public static final MapCodecHelper<Holder<StructurePoolElementType<?>>> STRUCTURE_POOL_ELEMENT = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_POOL_ELEMENT);\n    public static final MapCodecHelper<Holder<MapCodec<? extends PoolAliasBinding>>> POOL_ALIAS_BINDING_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POOL_ALIAS_BINDING_TYPE);\n    public static final MapCodecHelper<Holder<CatVariant>> CAT_VARIANT = RegistryMapCodecHelper.create(BuiltInRegistries.CAT_VARIANT);\n    public static final MapCodecHelper<Holder<FrogVariant>> FROG_VARIANT = RegistryMapCodecHelper.create(BuiltInRegistries.FROG_VARIANT);\n    public static final MapCodecHelper<Holder<Instrument>> INSTRUMENT = RegistryMapCodecHelper.create(BuiltInRegistries.INSTRUMENT);\n    public static final MapCodecHelper<Holder<DecoratedPotPattern>> DECORATED_POT_PATTERN = RegistryMapCodecHelper.create(BuiltInRegistries.DECORATED_POT_PATTERN);\n    public static final MapCodecHelper<Holder<CreativeModeTab>> CREATIVE_MODE_TAB = RegistryMapCodecHelper.create(BuiltInRegistries.CREATIVE_MODE_TAB);\n    public static final MapCodecHelper<Holder<CriterionTrigger<?>>> TRIGGER_TYPES = RegistryMapCodecHelper.create(BuiltInRegistries.TRIGGER_TYPES);\n    public static final MapCodecHelper<Holder<NumberFormatType<?>>> NUMBER_FORMAT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.NUMBER_FORMAT_TYPE);\n    public static final MapCodecHelper<Holder<ArmorMaterial>> ARMOR_MATERIAL = RegistryMapCodecHelper.create(BuiltInRegistries.ARMOR_MATERIAL);\n    public static final MapCodecHelper<Holder<DataComponentType<?>>> DATA_COMPONENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.DATA_COMPONENT_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends EntitySubPredicate>>> ENTITY_SUB_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENTITY_SUB_PREDICATE_TYPE);\n    public static final MapCodecHelper<Holder<ItemSubPredicate.Type<?>>> ITEM_SUB_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE);\n    public static final MapCodecHelper<Holder<MapDecorationType>> MAP_DECORATION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.MAP_DECORATION_TYPE);\n    public static final MapCodecHelper<Holder<DataComponentType<?>>> ENCHANTMENT_EFFECT_COMPONENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends LevelBasedValue>>> ENCHANTMENT_LEVEL_BASED_VALUE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_LEVEL_BASED_VALUE_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends EnchantmentEntityEffect>>> ENCHANTMENT_ENTITY_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_ENTITY_EFFECT_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends EnchantmentLocationBasedEffect>>> ENCHANTMENT_LOCATION_BASED_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_LOCATION_BASED_EFFECT_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends EnchantmentValueEffect>>> ENCHANTMENT_VALUE_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_VALUE_EFFECT_TYPE);\n    public static final MapCodecHelper<Holder<MapCodec<? extends EnchantmentProvider>>> ENCHANTMENT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_PROVIDER_TYPE);\n\n    // ENUMS\n    public static final MapCodecHelper<Rarity> ITEM_RARITY = new MapCodecHelper<>(enumerable(Rarity.class));\n    public static final MapCodecHelper<AttributeModifier.Operation> ATTRIBUTE_OPERATION = new MapCodecHelper<>(AttributeModifier.Operation.CODEC);\n    public static final MapCodecHelper<Direction> DIRECTION = new MapCodecHelper<>(enumerable(Direction.class));\n    public static final MapCodecHelper<Direction.Axis> AXIS = new MapCodecHelper<>(enumerable(Direction.Axis.class));\n    public static final MapCodecHelper<Direction.Plane> PLANE = new MapCodecHelper<>(enumerable(Direction.Plane.class));\n    public static final MapCodecHelper<MobCategory> MOB_CATEGORY = new MapCodecHelper<>(enumerable(MobCategory.class));\n    public static final MapCodecHelper<DyeColor> DYE_COLOR = new MapCodecHelper<>(enumerable(DyeColor.class));\n    public static final MapCodecHelper<SoundSource> SOUND_SOURCE = new MapCodecHelper<>(enumerable(SoundSource.class));\n    public static final MapCodecHelper<Difficulty> DIFFICULTY = new MapCodecHelper<>(enumerable(Difficulty.class));\n    public static final MapCodecHelper<EquipmentSlot> EQUIPMENT_SLOT = new MapCodecHelper<>(enumerable(EquipmentSlot.class));\n    public static final MapCodecHelper<Mirror> MIRROR = new MapCodecHelper<>(enumerable(Mirror.class));\n    public static final MapCodecHelper<Rotation> ROTATION = new MapCodecHelper<>(enumerable(Rotation.class));\n\n    // MINECRAFT TYPES\n    public static final MapCodecHelper<ResourceLocation> RESOURCE_LOCATION = new MapCodecHelper<>(ResourceLocation.CODEC);\n    public static final MapCodecHelper<CompoundTag> COMPOUND_TAG = new MapCodecHelper<>(CompoundTag.CODEC);\n    public static final MapCodecHelper<ItemStack> ITEM_STACK = new MapCodecHelper<>(ItemStack.CODEC);\n    public static final MapCodecHelper<ItemStack> ITEM_STACK_STRICT = new MapCodecHelper<>(ItemStack.STRICT_CODEC);\n    public static final MapCodecHelper<Component> TEXT = new MapCodecHelper<>(ComponentSerialization.CODEC);\n    public static final MapCodecHelper<BlockPos> BLOCK_POS = new MapCodecHelper<>(BlockPos.CODEC);\n    public static final MapCodecHelper<Ingredient> INGREDIENT = new MapCodecHelper<>(Ingredient.CODEC);\n    public static final MapCodecHelper<Ingredient> INGREDIENT_NONEMPTY = new MapCodecHelper<>(Ingredient.CODEC_NONEMPTY);\n    public static final MapCodec<BlockState> BLOCK_STATE_MAP_CODEC = Codec.mapPair(BLOCK.get().fieldOf(\"block\"), Codec.unboundedMap(Codec.STRING, Codec.STRING).optionalFieldOf(\"properties\")).flatXmap(MapCodecs::decodeBlockState, MapCodecs::encodeBlockState);\n    public static final MapCodecHelper<BlockState> BLOCK_STATE = new MapCodecHelper<>(BLOCK_STATE_MAP_CODEC.codec());\n    public static final MapCodecHelper<AttributeModifier> ATTRIBUTE_MODIFIER = new MapCodecHelper<>(AttributeModifier.CODEC);\n    public static final MapCodecHelper<MobEffectInstance> EFFECT_INSTANCE = new MapCodecHelper<>(MobEffectInstance.CODEC);\n    public static final MapCodecHelper<Vector3f> VECTOR_3F = new MapCodecHelper<>(ExtraCodecs.VECTOR3F);\n\n    // Bookshelf Types\n    public static final MapCodecHelper<ILoadCondition> LOAD_CONDITION = LoadConditions.CODEC_HELPER;\n\n    /**\n     * Creates a Codec that can flexibly read individual values as a list in addition to traditional lists.\n     *\n     * @param codec The codec for reading an individual value.\n     * @param <T>   The type of value handled by the codec.\n     * @return A Codec that can flexibly read individual values as a list in addition to traditional lists.\n     */\n    public static <T> Codec<List<T>> flexibleList(Codec<T> codec) {\n\n        return Codec.either(codec.listOf(), codec).xmap(either -> either.map(Function.identity(), List::of), list -> list.size() == 1 ? Either.right(list.getFirst()) : Either.left(list));\n    }\n\n    /**\n     * Creates a Codec that can flexibly read both individual values and arrays of values as a set.\n     *\n     * @param codec The Codec for reading an individual value.\n     * @param <T>   The type of value handled by the codec.\n     * @return A Codec that can flexibly read both individual values and arrays of values as a set.\n     */\n    public static <T> Codec<Set<T>> flexibleSet(Codec<T> codec) {\n\n        return flexibleList(codec).xmap(LinkedHashSet::new, ArrayList::new);\n    }\n\n    /**\n     * Creates a Codec that can flexibly read both individual values and arrays of values as an array.\n     *\n     * @param codec        The Codec for reading an individual value.\n     * @param arrayBuilder A function that creates new arrays of the required type. The function is given the size of\n     *                     the list.\n     * @param <T>          The type of value handled by the codec.\n     * @return A Codec that can flexibly read both individual values and arrays of values as an array.\n     */\n    public static <T> Codec<T[]> flexibleArray(Codec<T> codec, IntFunction<T[]> arrayBuilder) {\n\n        return flexibleList(codec).xmap(list -> list.toArray(arrayBuilder.apply(list.size())), List::of);\n    }\n\n    /**\n     * Creates a Codec that will use a fallback value if no other value is specified. This is different from\n     * {@link Codec#optionalFieldOf(String, Object)} in that the fallback value is provided by a supplier.\n     *\n     * @param codec            The base Codec to use.\n     * @param name             The name of the field to read from.\n     * @param fallbackSupplier A supplier that produces the default value. You should probably memoize this.\n     * @param <T>              The type of value handled by the codec.\n     * @return A Codec that will use a fallback value if the field is not specified.\n     */\n    public static <T> MapCodec<T> fallback(Codec<T> codec, String name, Supplier<T> fallbackSupplier) {\n\n        return fallback(codec, name, fallbackSupplier, true);\n    }\n\n    /**\n     * Creates a Codec that handles optional values. This is different from\n     * {@link Codec#optionalFieldOf(String, Object)} in that it keeps the type as an Optional.\n     *\n     * @param codec         The base Codec to use.\n     * @param name          The name of the field to read from.\n     * @param fallback      The fallback optional value.\n     * @param writesDefault Should the default value be written or left blank?\n     * @param <T>           The type of the optional value handled by the coded.\n     * @return A Codec that handles optional values.\n     */\n    public static <T> MapCodec<Optional<T>> optional(Codec<T> codec, String name, Optional<T> fallback, boolean writesDefault) {\n\n        return Codec.optionalField(name, codec, false).xmap(o -> o.isPresent() ? o : fallback, a -> a.isEmpty() || (Objects.equals(a.get(), fallback.orElse(null)) && !writesDefault) ? Optional.empty() : a);\n    }\n\n    /**\n     * Creates a Codec that can handle nullable values.\n     *\n     * @param codec     The base Codec to use.\n     * @param fieldName The name of the field to read from.\n     * @param <T>       The type of value handled by the codec.\n     * @return A Codec that handles nullable values.\n     */\n    public static <T> MapCodec<T> nullable(Codec<T> codec, String fieldName) {\n\n        return Codec.optionalField(fieldName, codec, false).xmap(optional -> optional.orElse(null), Optional::ofNullable);\n    }\n\n    /**\n     * Creates a Codec that will use a fallback value if no other value is specified. This is different from\n     * {@link Codec#optionalFieldOf(String, Object)} in that the fallback value is provided by a supplier. It also\n     * allows you to control if the default value should be written when or left blank.\n     *\n     * @param codec            The base Codec to use.\n     * @param name             The name of the field to read from.\n     * @param fallbackSupplier A supplier that produces the default value. You should probably memoize this.\n     * @param writesDefault    Should the default value be written or left blank?\n     * @param <T>              The type of value handled by the codec.\n     * @return A Codec that will use a fallback value if the field is not specified.\n     */\n    public static <T> MapCodec<T> fallback(Codec<T> codec, String name, Supplier<T> fallbackSupplier, boolean writesDefault) {\n\n        return Codec.optionalField(name, codec, false).xmap(value -> value.orElse(fallbackSupplier.get()), value -> {\n            final T fallback = fallbackSupplier.get();\n            return Objects.equals(value, fallback) && !writesDefault ? Optional.empty() : Optional.of(value);\n        });\n    }\n\n    private static <T extends Enum<T>> Map<String, T> getEnumsByName(Class<T> enumClass) {\n        if (!enumClass.isEnum()) {\n            throw new IllegalStateException(\"Class \" + enumClass.getCanonicalName() + \" is not an enum!\");\n        }\n        final Map<String, T> valueMap = new HashMap<>();\n        for (T value : enumClass.getEnumConstants()) {\n            final String name = value.name();\n            if (valueMap.containsKey(name)) {\n                Constants.LOG.error(\"Duplicate name '{}' found in enum '{}'. Another mod is doing something very wrong. old='{}' new='{}'\", name, enumClass.getName(), valueMap.get(name), value);\n            }\n            valueMap.put(name, value);\n        }\n        return valueMap;\n    }\n\n    /**\n     * Creates a Codec that handles enum values by using their enum constant names.\n     * <br>\n     * If the codec can not read an enum value from the provided name it will try again using an all uppercase version\n     * of the input. This allows users to write values in all lowercase which may feel more natural for some users and\n     * adds extra flexibility.\n     * <br>\n     * If the codec can not read any enum value from the provided name it will create an error. The error message will\n     * try to help the user by recommending a nearby match and including all possible values.\n     *\n     * @param enumClass The class of the enum to get values for.\n     * @param <T>       The type of the enum.\n     * @return A codec that can read and write enum values using their enum constant name.\n     */\n    public static <T extends Enum<T>> Codec<T> enumerable(Class<T> enumClass) {\n        final Map<String, T> enumValues = getEnumsByName(enumClass);\n        final Function<String, T> fromString = name -> {\n            T value = enumValues.get(name);\n            if (value == null) {\n                value = enumValues.get(name.toUpperCase(Locale.ROOT));\n            }\n            return value;\n        };\n        final UnaryOperator<String> errorMessage = name -> {\n            final StringJoiner message = new StringJoiner(\" \");\n            message.add(\"Unable to find \" + enumClass.getSimpleName() + \" entry \\\"\" + name + \"\\\".\");\n            final Set<String> similarMatches = TextHelper.getPossibleMatches(name, enumValues.keySet(), 2);\n            if (!similarMatches.isEmpty()) {\n                message.add(\"Did you mean \\\"\" + similarMatches.stream().findFirst().get() + \"\\\"?\");\n            }\n            message.add(\"Available Options are \" + TextHelper.formatCollection(enumValues.keySet()));\n            return message.toString();\n        };\n        return Codec.STRING.flatXmap(string -> Optionull.mapOrElse(fromString.apply(string), DataResult::success, () -> DataResult.error(() -> errorMessage.apply(string))), object -> DataResult.success(object.name()));\n    }\n\n    // INTERNAL HELPERS\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static DataResult<BlockState> decodeBlockState(Pair<Holder<Block>, Optional<Map<String, String>>> props) {\n\n        final Block block = props.getFirst().value();\n        final Map<String, String> properties = props.getSecond().orElse(new HashMap<>());\n        BlockState state = block.defaultBlockState();\n\n        if (!properties.isEmpty()) {\n\n            final StateDefinition<Block, BlockState> definition = block.getStateDefinition();\n\n            for (Map.Entry<String, String> entry : properties.entrySet()) {\n\n                final Property<? extends Comparable<?>> property = definition.getProperty(entry.getKey());\n\n                if (property != null) {\n\n                    final Optional<?> value = property.getValue(entry.getValue());\n\n                    if (value.isPresent()) {\n\n                        try {\n\n                            state = state.setValue((Property) property, (Comparable) value.get());\n                        }\n\n                        catch (final Exception e) {\n\n                            Constants.LOG.error(\"Failed to update state for block {} with valid value {}={}. The mod that adds this block may have a serious issue.\", BuiltInRegistries.BLOCK.getKey(block), entry.getKey(), entry.getValue());\n                            return DataResult.error(e::getMessage);\n                        }\n                    }\n\n                    else {\n\n                        return DataResult.error(() -> \"\\\"\" + entry.getValue() + \"\\\" is not a valid value for property \\\"\" + property.getName() + \"\\\" on block \\\"\" + BuiltInRegistries.BLOCK.getKey(block) + \"\\\". Available values: \" + property.getAllValues().map(propVal -> ((Property) property).getName(propVal.value())).collect(Collectors.joining()));\n                    }\n                }\n\n                else {\n\n                    return DataResult.error(() -> \"The property \\\"\" + entry.getKey() + \"\\\" is not valid for block \\\"\" + BuiltInRegistries.BLOCK.getKey(block) + \"\\\". Available properties: \" + definition.getProperties().stream().map(Property::getName).collect(Collectors.joining()));\n                }\n            }\n        }\n\n        return DataResult.success(state);\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static DataResult<Pair<Holder<Block>, Optional<Map<String, String>>>> encodeBlockState(BlockState state) {\n\n        final Map<String, String> propertyMap = new HashMap<>();\n\n        for (Map.Entry<Property<?>, Comparable<?>> entry : state.getValues().entrySet()) {\n\n            propertyMap.put(entry.getKey().getName(), ((Property) entry.getKey()).getName(entry.getValue()));\n        }\n\n        return DataResult.success(new Pair<>(state.getBlock().builtInRegistryHolder(), Optional.ofNullable(propertyMap.isEmpty() ? null : propertyMap)));\n    }\n\n    /**\n     * Creates a codec that will try two different codecs, using the first valid codec. Encoding will always use the\n     * first codec.\n     *\n     * @param first  The first codec to try when decoding. This will be the only codec used in encoding.\n     * @param second The second codec to try when decoding.\n     * @param <T>    The type of codec to create.\n     * @return A codec that will try two different codecs.\n     */\n    public static <T> Codec<T> xor(Codec<T> first, Codec<T> second) {\n        return Codec.xor(first, second).xmap(FunctionHelper::unpack, Either::left);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/RegistryMapCodecHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.data.codecs.map;\n\nimport com.mojang.serialization.Codec;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.RegistryFixedCodec;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.tags.TagKey;\n\npublic class RegistryMapCodecHelper<T> extends MapCodecHelper<Holder<T>> {\n\n    private final MapCodecHelper<TagKey<T>> tagHelper;\n\n    private RegistryMapCodecHelper(Codec<Holder<T>> holderCodec, ResourceKey<Registry<T>> key) {\n        super(holderCodec);\n        this.tagHelper = new MapCodecHelper<>(TagKey.codec(key));\n    }\n\n    public MapCodecHelper<TagKey<T>> tag() {\n        return this.tagHelper;\n    }\n\n    /**\n     * Creates a Codec helper for a builtin registry.\n     *\n     * @param registry The registry to create a codec helper for.\n     * @param <T>      The type of value held by the registry.\n     * @return A Codec helper for a builtin registry.\n     */\n    public static <T> RegistryMapCodecHelper<T> create(Registry<T> registry) {\n        return new RegistryMapCodecHelper<>(registry.holderByNameCodec(), (ResourceKey<Registry<T>>) registry.key());\n    }\n\n    /**\n     * Creates a Codec helper for a datapack registry. This codec can only be used when registry access is available\n     * through RegistryOps.\n     *\n     * @param key The key of the registry to use.\n     * @param <T> The type of value held by the registry.\n     * @return A Codec helper for datapack entries.\n     */\n    public static <T> RegistryMapCodecHelper<T> create(ResourceKey<Registry<T>> key) {\n        return new RegistryMapCodecHelper<>(RegistryFixedCodec.create(key), key);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/stream/StreamCodecs.java",
    "content": "package net.darkhax.bookshelf.common.api.data.codecs.stream;\n\nimport io.netty.buffer.ByteBuf;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.FalseIngredient;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.Ingredient;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StreamCodecs {\n\n    public static final StreamCodec<RegistryFriendlyByteBuf, String> STRING = StreamCodec.of(FriendlyByteBuf::writeUtf, FriendlyByteBuf::readUtf);\n    public static final StreamCodec<RegistryFriendlyByteBuf, List<ItemStack>> ITEM_STACK_LIST = list(ItemStack.STREAM_CODEC);\n    public static final StreamCodec<RegistryFriendlyByteBuf, Ingredient> INGREDIENT_NON_EMPTY = StreamCodec.of(\n            Ingredient.CONTENTS_STREAM_CODEC,\n            buf -> {\n                final Ingredient ingredient = Ingredient.CONTENTS_STREAM_CODEC.decode(buf);\n                return ingredient.isEmpty() ? FalseIngredient.INSTANCE.get() : ingredient;\n            }\n    );\n\n    public static <B extends ByteBuf, V> StreamCodec<B, List<V>> list(StreamCodec<B, V> baseCodec) {\n        return StreamCodec.of(\n                (buf, val) -> {\n                    buf.writeInt(val.size());\n                    for (V entry : val) {\n                        baseCodec.encode(buf, entry);\n                    }\n                },\n                buf -> {\n                    final int size = buf.readInt();\n                    final List<V> list = new ArrayList<>(size);\n                    for (int i = 0; i < size; i++) {\n                        list.add(baseCodec.decode(buf));\n                    }\n                    return list;\n                }\n        );\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/ConditionType.java",
    "content": "package net.darkhax.bookshelf.common.api.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * Represents a type of load condition that Bookshelf can process and test.\n *\n * @param id    The ID of the condition type.\n * @param codec The codec used to serialize the condition from data.\n */\npublic record ConditionType(ResourceLocation id, MapCodec<? extends ILoadCondition> codec) {\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/ILoadCondition.java",
    "content": "package net.darkhax.bookshelf.common.api.data.conditions;\n\n/**\n * Load conditions allow JSON entries in data/resource packs to define optional conditions in order for them to load.\n * For example a recipe file can prevent loading if a required item is not registered.\n */\npublic interface ILoadCondition {\n\n    /**\n     * Tests if the condition has been met or not.\n     *\n     * @return Has the condition been met?\n     */\n    boolean allowLoading();\n\n    /**\n     * Gets the type of the condition. This is required for serializing conditions.\n     *\n     * @return The type of the condition.\n     */\n    ConditionType getType();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/LoadConditions.java",
    "content": "package net.darkhax.bookshelf.common.api.data.conditions;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.JsonOps;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecHelper;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class LoadConditions {\n\n    private static final Map<ResourceLocation, ConditionType> CONDITION_TYPES = new HashMap<>();\n    private static final Codec<ConditionType> CONDITION_TYPE_CODEC = ResourceLocation.CODEC.xmap(CONDITION_TYPES::get, ConditionType::id);\n    public static final String LOAD_CONDITION_TAG = Constants.id(\"load_conditions\").toString();\n    public static final Codec<ILoadCondition> CONDITION_CODEC = CONDITION_TYPE_CODEC.dispatch(ILoadCondition::getType, ConditionType::codec);\n    public static final MapCodecHelper<ILoadCondition> CODEC_HELPER = new MapCodecHelper<>(CONDITION_CODEC);\n\n    @Nullable\n    public static ConditionType getType(ResourceLocation id) {\n        return CONDITION_TYPES.get(id);\n    }\n\n    public static <T extends ILoadCondition> ConditionType register(ResourceLocation id, MapCodec<T> codec) {\n        if (CONDITION_TYPES.containsKey(id)) {\n            Constants.LOG.warn(\"JSON Load Serializer ID {} has already been assigned to {}. Replacing with {}.\", id, CONDITION_TYPES.get(id).codec(), codec);\n        }\n        final ConditionType type = new ConditionType(id, codec);\n        CONDITION_TYPES.put(id, type);\n        return type;\n    }\n\n    /**\n     * Reads one or more conditions from a JSON element. If the element is an object an array of 1 will be returned.\n     *\n     * @param conditionData The condition data read from the raw JSON entry.\n     * @return An array of load conditions read from the data.\n     */\n    public static ILoadCondition[] getConditions(JsonElement conditionData) {\n        return MapCodecs.LOAD_CONDITION.getArray().decode(JsonOps.INSTANCE, conditionData).getOrThrow().getFirst();\n    }\n\n    /**\n     * Tests if a raw JSON element can be loaded. This will search for the condition property and attempt to deserialize\n     * and test those conditions.\n     *\n     * @param rawJson The raw JSON data as read from the data/resource pack.\n     * @return Whether the JSON entry should be loaded or not.\n     */\n    public static boolean canLoad(JsonObject rawJson) {\n        if (rawJson.has(LOAD_CONDITION_TAG)) {\n            final JsonElement conditionData = rawJson.get(LOAD_CONDITION_TAG);\n            for (ILoadCondition condition : getConditions(conditionData)) {\n                if (!condition.allowLoading()) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/enchantment/EnchantmentLevel.java",
    "content": "package net.darkhax.bookshelf.common.api.data.enchantment;\n\nimport it.unimi.dsi.fastutil.objects.Object2IntMap;\nimport net.minecraft.core.Holder;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.enchantment.Enchantment;\nimport net.minecraft.world.item.enchantment.ItemEnchantments;\n\nimport java.util.function.ToIntBiFunction;\n\n/**\n * Calculates enchantment levels using different methods.\n */\npublic enum EnchantmentLevel {\n\n    /**\n     * Returns the highest level among all matching enchantments.\n     */\n    HIGHEST((tag, enchantments) -> {\n        int level = 0;\n        for (Object2IntMap.Entry<Holder<Enchantment>> entry : enchantments.entrySet()) {\n            if (entry.getKey().is(tag) && entry.getIntValue() > level) {\n                level = entry.getIntValue();\n            }\n        }\n        return level;\n    }),\n\n    /**\n     * Returns the lowest level among all matching enchantments.\n     */\n    LOWEST((tag, enchantments) -> {\n        int level = Integer.MAX_VALUE;\n        for (Object2IntMap.Entry<Holder<Enchantment>> entry : enchantments.entrySet()) {\n            if (entry.getKey().is(tag) && entry.getIntValue() < level) {\n                level = entry.getIntValue();\n            }\n        }\n        return level;\n    }),\n\n    /**\n     * Returns the level of the first matching enchantment.\n     */\n    FIRST((tag, enchantments) -> {\n        for (Object2IntMap.Entry<Holder<Enchantment>> entry : enchantments.entrySet()) {\n            if (entry.getKey().is(tag)) {\n                return entry.getIntValue();\n            }\n        }\n        return 0;\n    }),\n\n    /**\n     * Returns the combined level of all matching enchantments.\n     */\n    CUMULATIVE((tag, enchantments) -> {\n        int level = 0;\n        for (Object2IntMap.Entry<Holder<Enchantment>> entry : enchantments.entrySet()) {\n            if (entry.getKey().is(tag)) {\n                level += entry.getIntValue();\n            }\n        }\n        return level;\n    });\n\n    private final ToIntBiFunction<TagKey<Enchantment>, ItemEnchantments> func;\n\n    EnchantmentLevel(ToIntBiFunction<TagKey<Enchantment>, ItemEnchantments> func) {\n        this.func = func;\n    }\n\n    /**\n     * Gets the level of matching enchantments based on the calculation type.\n     *\n     * @param enchType A tag of the enchantments to match on.\n     * @param stack    The item to test.\n     * @return The level based on the calculation type.\n     */\n    public int get(TagKey<Enchantment> enchType, ItemStack stack) {\n        return (!stack.isEmpty() && stack.isEnchanted()) ? this.func.applyAsInt(enchType, stack.getEnchantments()) : 0;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/ingredient/IngredientLogic.java",
    "content": "package net.darkhax.bookshelf.common.api.data.ingredient;\n\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic interface IngredientLogic<T extends IngredientLogic<T>> {\n\n    boolean test(ItemStack stack);\n\n    default List<ItemStack> getAllMatchingStacks() {\n        final List<ItemStack> matching = new ArrayList<>();\n        for (Item item : BuiltInRegistries.ITEM) {\n            final ItemStack stack = item.getDefaultInstance();\n            if (this.test(stack)) {\n                matching.add(stack);\n            }\n        }\n        return matching;\n    }\n\n    default boolean requiresTesting() {\n        return true;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/loot/PoolTarget.java",
    "content": "package net.darkhax.bookshelf.common.api.data.loot;\n\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.BuiltInLootTables;\nimport net.minecraft.world.level.storage.loot.LootTable;\n\n/**\n * Represents a specific loot pool target within a loot table. This class also provides predefined constants for some of\n * the commonly modified loot pools.\n *\n * @param table The id of the loot table to target.\n * @param index The index of the pool within the loot table. This is usually based on the order pools appear in the JSON\n *              data.\n * @param hash  A hash of the pools JSON data. This can be obtained using the bookshelf debug command in a development\n *              environment.\n */\npublic record PoolTarget(ResourceLocation table, int index, int hash) {\n\n    public static final PoolTarget MINESHAFT_RARE = of(BuiltInLootTables.ABANDONED_MINESHAFT, 0, 1537257923);\n    public static final PoolTarget MINESHAFT_UNCOMMON = of(BuiltInLootTables.ABANDONED_MINESHAFT, 1, -444048389);\n    public static final PoolTarget MINESHAFT_COMMON = of(BuiltInLootTables.ABANDONED_MINESHAFT, 2, 634581377);\n\n    public static final PoolTarget SIMPLE_DUNGEON_RARE = of(BuiltInLootTables.SIMPLE_DUNGEON, 0, -66091299);\n    public static final PoolTarget SIMPLE_DUNGEON_UNCOMMON = of(BuiltInLootTables.SIMPLE_DUNGEON, 1, 1870100239);\n    public static final PoolTarget SIMPLE_DUNGEON_COMMON = of(BuiltInLootTables.SIMPLE_DUNGEON, 2, 2004993944);\n\n    public static final PoolTarget CAT_GIFT = of(BuiltInLootTables.CAT_MORNING_GIFT, 0, 234355958);\n\n    public static final PoolTarget FISHING = of(BuiltInLootTables.FISHING, 0, 1127209674);\n    public static final PoolTarget FISHING_FISH = of(BuiltInLootTables.FISHING_FISH, 0, -190358337);\n    public static final PoolTarget FISHING_JUNK = of(BuiltInLootTables.FISHING_JUNK, 0, 1154453499);\n    public static final PoolTarget FISHING_TREASURE = of(BuiltInLootTables.FISHING_TREASURE, 0, 1729324233);\n\n    public static final PoolTarget PIGLIN_BARTERING = of(BuiltInLootTables.PIGLIN_BARTERING, 0, 718156885);\n\n    public static final PoolTarget SNIFFER_DIGGING = of(BuiltInLootTables.SNIFFER_DIGGING, 0, 1185470198);\n\n    public static final PoolTarget ARCHAEOLOGY_PYRAMID = of(BuiltInLootTables.DESERT_PYRAMID_ARCHAEOLOGY, 0, -1867551069);\n    public static final PoolTarget ARCHAEOLOGY_DESERT_WELL = of(BuiltInLootTables.DESERT_WELL_ARCHAEOLOGY, 0, -1508422416);\n    public static final PoolTarget ARCHAEOLOGY_OCEAN_RUIN_COLD = of(BuiltInLootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY, 0, -1117683719);\n    public static final PoolTarget ARCHAEOLOGY_OCEAN_RUIN_WARM = of(BuiltInLootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY, 0, 153317912);\n    public static final PoolTarget ARCHAEOLOGY_TRAIL_RUINS_COMMON = of(BuiltInLootTables.TRAIL_RUINS_ARCHAEOLOGY_COMMON, 0, 300798809);\n    public static final PoolTarget ARCHAEOLOGY_TRAIL_RUINS_RARE = of(BuiltInLootTables.TRAIL_RUINS_ARCHAEOLOGY_RARE, 0, 1848809003);\n\n    public static PoolTarget of(ResourceKey<LootTable> table, int index, int hash) {\n        return new PoolTarget(table.location(), index, hash);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/data/loot/modifiers/LootPoolAddition.java",
    "content": "package net.darkhax.bookshelf.common.api.data.loot.modifiers;\n\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\n\n/**\n * Represents a loot pool entry that should be added to a loot pool.\n *\n * @param id    A unique ID for the individual entry. This should be unique to each entry and is used to help identify\n *              and debug entry additions.\n * @param entry The entry to add to the pool.\n */\npublic record LootPoolAddition(ResourceLocation id, LootPoolEntryContainer entry) {\n\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/MerchantTier.java",
    "content": "package net.darkhax.bookshelf.common.api.entity.villager;\n\npublic enum MerchantTier {\n\n    NOVICE(0),\n    APPRENTICE(10),\n    JOURNEYMAN(70),\n    EXPERT(150),\n    MASTER(250);\n\n    private final int requiredExp;\n\n    MerchantTier(int requiredExp) {\n\n        this.requiredExp = requiredExp;\n    }\n\n    public int getRequiredExp() {\n        return this.requiredExp;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerBuys.java",
    "content": "package net.darkhax.bookshelf.common.api.entity.villager.trades;\n\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.npc.VillagerTrades;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.item.trading.ItemCost;\nimport net.minecraft.world.item.trading.MerchantOffer;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\n\n/**\n * A simple villager trade entry that represents an item being bought by the villager.\n *\n * @param stackToBuy      The item being bought by the villager.\n * @param emeralds        The amount of emeralds to award the player.\n * @param maxUses         The amount of times the trade can be performed before a restocking is required.\n * @param villagerXp      The amount of villager XP to award for performing the trade.\n * @param priceMultiplier A price multiplier.\n */\npublic record VillagerBuys(Supplier<ItemCost> stackToBuy, int emeralds, int maxUses, int villagerXp, float priceMultiplier) implements VillagerTrades.ItemListing {\n\n    @Override\n    public MerchantOffer getOffer(@NotNull Entity entity, @NotNull RandomSource random) {\n        return new MerchantOffer(this.stackToBuy.get(), new ItemStack(Items.EMERALD, this.emeralds), this.maxUses, this.villagerXp, this.priceMultiplier);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerOffers.java",
    "content": "package net.darkhax.bookshelf.common.api.entity.villager.trades;\n\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.npc.VillagerTrades;\nimport net.minecraft.world.item.trading.MerchantOffer;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\n\n/**\n * A simple villager trade entry that selects a random offer from an equally weighted array of offers.\n *\n * @param offers An equally weighted array of offers.\n */\npublic record VillagerOffers(Supplier<MerchantOffer>... offers) implements VillagerTrades.ItemListing {\n\n    @SafeVarargs\n    public VillagerOffers {\n    }\n\n    @Override\n    public MerchantOffer getOffer(@NotNull Entity entity, @NotNull RandomSource randomSource) {\n        return offers[randomSource.nextInt(offers.length)].get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerSells.java",
    "content": "package net.darkhax.bookshelf.common.api.entity.villager.trades;\n\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.npc.VillagerTrades;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.item.trading.ItemCost;\nimport net.minecraft.world.item.trading.MerchantOffer;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\n\n/**\n * A simple villager trade entry that represents an item being sold to the player.\n *\n * @param itemToBuy       The item being sold by the villager.\n * @param emeraldCost     The amount of emeralds the trade will cost.\n * @param maxUses         The amount of times the trade can be performed before a restocking is required.\n * @param villagerXp      The amount of villager XP to award for performing the trade.\n * @param priceMultiplier A price multiplier.\n */\npublic record VillagerSells(Supplier<ItemStack> itemToBuy, int emeraldCost, int maxUses, int villagerXp, float priceMultiplier) implements VillagerTrades.ItemListing {\n\n    @Override\n    public MerchantOffer getOffer(@NotNull Entity villager, @NotNull RandomSource rng) {\n        return new MerchantOffer(new ItemCost(Items.EMERALD, this.emeraldCost), this.itemToBuy.get(), this.maxUses, this.villagerXp, this.priceMultiplier);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/CachedSupplier.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * A Supplier implementation that will cache the result of an internal delegate supplier.\n *\n * @param <T> The type that is cached by the supplier.\n */\npublic class CachedSupplier<T> implements Supplier<T> {\n\n    /**\n     * The internal Supplier responsible for producing the cached value.\n     */\n    private final Supplier<T> delegate;\n\n    /**\n     * Tracks if the delegate supplier has been cached.\n     */\n    private boolean cached = false;\n\n    /**\n     * The most recently cached value.\n     */\n    @Nullable\n    private T cachedValue;\n\n    protected CachedSupplier(Supplier<T> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public T get() {\n        if (!this.isCached()) {\n            this.cachedValue = this.delegate.get();\n            this.cached = true;\n        }\n        return cachedValue;\n    }\n\n    /**\n     * Invalidates the cached value. This will result in a new value being cached the next type {@link #get()} is used.\n     */\n    public void invalidate() {\n        this.cached = false;\n        this.cachedValue = null;\n    }\n\n    /**\n     * Checks if this supplier has a cached value. This is not a substitute for null checking.\n     *\n     * @return Has the supplier cached a value.\n     */\n    public boolean isCached() {\n        return this.cached;\n    }\n\n    /**\n     * Safely attempts to invoke a consumer with the cached value. If a value has not been cached the consumer will not\n     * be invoked and a new value will not be cached. The consumer will still be invoked if the cached value is null.\n     *\n     * @param consumer The consumer to invoke if a value has been cached.\n     */\n    public void ifCached(Consumer<T> consumer) {\n        if (this.isCached()) {\n            consumer.accept(this.get());\n        }\n    }\n\n    /**\n     * Safely attempts to invoke a consumer with the cached value. If a value has not been cached, or the cached value\n     * is null the consumer will not be invoked and a new value will not be cached.\n     *\n     * @param consumer The consumer to invoke if a cached value is present.\n     */\n    public void ifPresent(Consumer<T> consumer) {\n        if (this.cachedValue != null) {\n            consumer.accept(this.get());\n        }\n    }\n\n    /**\n     * Invokes the consumer with the cached value. This will cause a value to be cached if one has not been cached\n     * already.\n     *\n     * @param consumer The consumer to invoke.\n     */\n    public void apply(Consumer<T> consumer) {\n        consumer.accept(this.get());\n    }\n\n    /**\n     * Performs an unsafe cast to the expected type.\n     */\n    public <X> CachedSupplier<X> cast() {\n        return (CachedSupplier<X>) this;\n    }\n\n    /**\n     * Creates a cached supplier that can only produce a single value.\n     *\n     * @param singleton The only value for the cache to use.\n     * @param <T>       The type of value held by the cache.\n     * @return A cached supplier that will only produce a single cached value.\n     */\n    public static <T> CachedSupplier<T> singleton(T singleton) {\n        return cache(() -> singleton);\n    }\n\n    /**\n     * Creates a cached supplier that will cache a value from the supplied delegate when queried.\n     *\n     * @param delegate The delegate supplier responsible for producing the cached value. This will only be accessed when\n     *                 the cached supplier is being accessed and has not been cached already.\n     * @param <T>      The type of value held by the cache.\n     * @return A supplier that will cache a value from the supplied delegate supplier.\n     */\n    public static <T> CachedSupplier<T> cache(Supplier<T> delegate) {\n        return new CachedSupplier<>(delegate);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> CachedSupplier<T> of(ResourceKey<T> key) {\n        return CachedSupplier.cache(() -> {\n            final Registry<?> registry = BuiltInRegistries.REGISTRY.get(key.registry());\n            if (registry == null) {\n                Constants.LOG.error(\"Registry {} could not be found!\", key.registry());\n                throw new IllegalStateException(\"Registry with name \" + key.registry() + \" was not found!\");\n            }\n            return ((Registry<T>) registry).getOrThrow(key);\n        });\n    }\n    \n    public static <T> CachedSupplier<T> of(Registry<T> registry, String namespace, String path) {\n        return of(registry, ResourceLocation.fromNamespaceAndPath(namespace, path));\n    }\n\n    public static <T> CachedSupplier<T> of(Registry<T> registry, ResourceLocation id) {\n        return CachedSupplier.cache(() -> registry.get(id));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/QuadConsumer.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport java.util.Objects;\n\n/**\n * A consumer that accepts four parameters.\n *\n * @param <P1> The first parameter.\n * @param <P2> The second parameter.\n * @param <P3> The third parameter.\n * @param <P4> The fourth parameter.\n */\n@FunctionalInterface\npublic interface QuadConsumer<P1, P2, P3, P4> {\n\n    /**\n     * Consumes the parameters.\n     *\n     * @param p1 The first parameter.\n     * @param p2 The second parameter.\n     * @param p3 The third parameter.\n     * @param p4 The fourth parameter.\n     */\n    void accept(P1 p1, P2 p2, P3 p3, P4 p4);\n\n    /**\n     * Chains another consumer on to this one.\n     *\n     * @param after The consumer to run after this one.\n     * @return A new consumer that chains both.\n     */\n    default QuadConsumer<P1, P2, P3, P4> andThen(QuadConsumer<P1, P2, P3, P4> after) {\n        Objects.requireNonNull(after);\n        return (p1, p2, p3, p4) -> {\n            accept(p1, p2, p3, p4);\n            after.accept(p1, p2, p3, p4);\n        };\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/ReloadableCache.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.common.mixin.access.level.AccessorRecipeManager;\nimport net.minecraft.core.Registry;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.LivingEntity;\nimport net.minecraft.world.item.crafting.Recipe;\nimport net.minecraft.world.item.crafting.RecipeHolder;\nimport net.minecraft.world.item.crafting.RecipeType;\nimport net.minecraft.world.level.Level;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * A cached value that is lazily loaded and will be invalidated automatically after the game has been reloaded.\n *\n * @param <T> The type of the cached value.\n */\npublic class ReloadableCache<T> implements Function<Level, T> {\n\n    /**\n     * A reloadable cache that will always return null. Not all empty instances will match this instance.\n     */\n    @SuppressWarnings(\"rawtypes\")\n    public static final ReloadableCache EMPTY = ReloadableCache.of(level -> null);\n\n    /**\n     * An internal function that is responsible for producing the value to cache.\n     */\n    private final Function<Level, T> delegate;\n\n    /**\n     * A flag that tracks if a value has been cached.\n     */\n    private boolean cached = false;\n\n    private int revision = 0;\n\n    /**\n     * The value that is currently cached.\n     */\n    @Nullable\n    private T cachedValue;\n\n    protected ReloadableCache(Function<Level, T> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Nullable\n    @Override\n    public T apply(Level level) {\n        if (!this.isCached() || this.revision != (level.isClientSide ? Constants.CLIENT_REVISION : Constants.SERVER_REVISION)) {\n            this.cachedValue = this.delegate.apply(level);\n            this.revision = (level.isClientSide) ? Constants.CLIENT_REVISION : Constants.SERVER_REVISION;\n            this.cached = true;\n        }\n        return this.cachedValue;\n    }\n\n    /**\n     * Manually invalidates the cache. This will result in a new value being cached the next time {@link #apply(Level)}\n     * is invoked.\n     */\n    public void invalidate() {\n        this.cached = false;\n        this.cachedValue = null;\n        this.revision = -1;\n    }\n\n    /**\n     * Checks if the cache has cached a value. This is not a substitute for null checking.\n     *\n     * @return Has the supplier cached a value.\n     */\n    public boolean isCached() {\n        return this.cached;\n    }\n\n    /**\n     * Invokes the consumer with the cached value. This will cause a value to be cached if one has not been cached\n     * already.\n     *\n     * @param level    The current game level. This is used to provide context about the current state of the game.\n     * @param consumer The consumer to invoke.\n     */\n    public void apply(Level level, Consumer<T> consumer) {\n        consumer.accept(this.apply(level));\n    }\n\n    /**\n     * Applies a function to the cached value if the value is not null.\n     *\n     * @param level    The current game level. This is used to provide context about the current state of the game.\n     * @param consumer The consumer to invoke.\n     */\n    public void ifPresent(Level level, Consumer<T> consumer) {\n        final T value = this.apply(level);\n        if (value != null) {\n            consumer.accept(value);\n        }\n    }\n\n    /**\n     * Maps non null cache values to a new value.\n     *\n     * @param level  The current game level. This is used to provide context about the current state of the game.\n     * @param mapper A mapper function to map the cached value to something new. This is only used if the value is not\n     *               null.\n     * @param <R>    The return type.\n     * @return The mapped value or null.\n     */\n    @Nullable\n    public <R> R map(Level level, Function<T, R> mapper) {\n        final T value = this.apply(level);\n        return value != null ? mapper.apply(value) : null;\n    }\n\n    /**\n     * Creates a cache of a value that will be recalculated when the game reloads.\n     *\n     * @param supplier The supplier used to produce the cached value.\n     * @param <T>      The type of value held by the cache.\n     * @return A cache of a value that will be recalculated when the game reloads.\n     */\n    public static <T> ReloadableCache<T> of(Supplier<T> supplier) {\n        return new ReloadableCache<>(level -> supplier.get());\n    }\n\n    /**\n     * Creates a cache of a value that will be recalculated when the game reloads.\n     *\n     * @param delegate A function used to produce the value to cache.\n     * @param <T>      The type of value held by the cache.\n     * @return A cache of a value that will be recalculated when the game reloads.\n     */\n    public static <T> ReloadableCache<T> of(Function<Level, T> delegate) {\n        return new ReloadableCache<>(delegate);\n    }\n\n    /**\n     * Creates a cache of a registry value that will be reaquired when the game reloads.\n     *\n     * @param registry The registry to look up the value in.\n     * @param id       The ID of the value to lookup.\n     * @param <T>      The type of value held by the cache.\n     * @return A cache of a registry value that will be reaquired when the game reloads.\n     */\n    public static <T> ReloadableCache<T> of(ResourceKey<? extends Registry<T>> registry, ResourceLocation id) {\n        return ReloadableCache.of(level -> level.registryAccess().registryOrThrow(registry).get(id));\n    }\n\n    /**\n     * Creates a cache of recipe entries for a recipe type.\n     *\n     * @param type The type of recipe.\n     * @param <T>  The type of the recipe.\n     * @return A map of recipes for the recipe type.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Recipe<?>> ReloadableCache<Map<ResourceLocation, RecipeHolder<T>>> of(RecipeType<T> type) {\n        return ReloadableCache.of(level -> {\n            final Map<ResourceLocation, RecipeHolder<T>> byId = new HashMap<>();\n            if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) {\n                final Collection<RecipeHolder<?>> recipes = accessor.bookshelf$byTypeMap().get(type);\n                recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder<T>) entry));\n            }\n            return byId;\n        });\n    }\n\n    /**\n     * Creates a cache of recipe entries for a recipe type.\n     *\n     * @param type The type of recipe.\n     * @param <T>  The type of the recipe.\n     * @return A map of recipes for the recipe type.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Recipe<?>> ReloadableCache<Map<ResourceLocation, RecipeHolder<T>>> recipes(Supplier<RecipeType<T>> type) {\n        return ReloadableCache.of(level -> {\n            final Map<ResourceLocation, RecipeHolder<T>> byId = new HashMap<>();\n            if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) {\n                final Collection<RecipeHolder<?>> recipes = accessor.bookshelf$byTypeMap().get(type.get());\n                recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder<T>) entry));\n            }\n            return byId;\n        });\n    }\n\n    /**\n     * Creates a cache of an entity instance.\n     *\n     * @param entityData The data used to create the entity.\n     * @return A reloadable entity instance.\n     */\n    public static ReloadableCache<Entity> entity(CompoundTag entityData) {\n        if (entityData == null || !entityData.contains(\"id\", Tag.TAG_STRING)) {\n            throw new IllegalStateException(\"The provided entity data does not contain an entity ID! data=\" + entityData);\n        }\n        return ReloadableCache.of(level -> {\n            try {\n                return EntityType.loadEntityRecursive(entityData, level, Function.identity());\n            }\n            catch (Exception e) {\n                throw new IllegalStateException(\"Encountered an error while constructing the target entity.\", e);\n            }\n        });\n    }\n\n    /**\n     * Creates a cache of a living entity instance.\n     *\n     * @param entityData The data used to create the entity.\n     * @return A reloadable living entity instance.\n     */\n    public static ReloadableCache<LivingEntity> living(CompoundTag entityData) {\n        final ReloadableCache<Entity> entityCache = entity(entityData);\n        return ReloadableCache.of(level -> {\n            if (entityCache.apply(level) instanceof LivingEntity living) {\n                return living;\n            }\n            throw new IllegalStateException(\"Constructed entity was not a LivingEntity type. data=\" + entityData);\n        });\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/SidedReloadableCache.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport net.darkhax.bookshelf.common.mixin.access.level.AccessorRecipeManager;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.crafting.Recipe;\nimport net.minecraft.world.item.crafting.RecipeHolder;\nimport net.minecraft.world.item.crafting.RecipeType;\nimport net.minecraft.world.level.Level;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\npublic class SidedReloadableCache<T> implements Function<Level, T> {\n\n    private final ReloadableCache<T> client;\n    private final ReloadableCache<T> server;\n\n    public SidedReloadableCache(ReloadableCache<T> client, ReloadableCache<T> server) {\n        this.client = client;\n        this.server = server;\n    }\n\n    public ReloadableCache<T> getCache(Level level) {\n        return level.isClientSide ? client : server;\n    }\n\n    @Nullable\n    @Override\n    public T apply(Level level) {\n        return getCache(level).apply(level);\n    }\n\n    public void invalidate(Level level) {\n        getCache(level).invalidate();\n    }\n\n    public boolean isCached(Level level) {\n        return getCache(level).isCached();\n    }\n\n    public void apply(Level level, Consumer<T> consumer) {\n        getCache(level).apply(level, consumer);\n    }\n\n    public void ifPresent(Level level, Consumer<T> consumer) {\n        getCache(level).ifPresent(level, consumer);\n    }\n\n    @Nullable\n    public <R> R map(Level level, Function<T, R> mapper) {\n        return getCache(level).map(level, mapper);\n    }\n\n    public static <T> SidedReloadableCache<T> of(Function<Level, T> cacheFunc) {\n        return new SidedReloadableCache<>(new ReloadableCache<>(cacheFunc), new ReloadableCache<>(cacheFunc));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Recipe<?>> SidedReloadableCache<Map<ResourceLocation, RecipeHolder<T>>> recipes(Supplier<RecipeType<T>> type) {\n        return of(level -> {\n            final Map<ResourceLocation, RecipeHolder<T>> byId = new HashMap<>();\n            if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) {\n                final Collection<RecipeHolder<?>> recipes = accessor.bookshelf$byTypeMap().get(type.get());\n                recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder<T>) entry));\n            }\n            return byId;\n        });\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/TriConsumer.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport java.util.Objects;\n\n@FunctionalInterface\npublic interface TriConsumer<P1, P2, P3> {\n\n    void accept(P1 p1, P2 p2, P3 p3);\n\n    default TriConsumer<P1, P2, P3> andThen(TriConsumer<P1, P2, P3> after) {\n\n        Objects.requireNonNull(after);\n\n        return (p1, p2, p3) -> {\n            accept(p1, p2, p3);\n            after.accept(p1, p2, p3);\n        };\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/function/TriFunction.java",
    "content": "package net.darkhax.bookshelf.common.api.function;\n\nimport java.util.Objects;\nimport java.util.function.Function;\n\n@FunctionalInterface\npublic interface TriFunction<P1, P2, P3, R> {\n\n    R apply(P1 p1, P2 p2, P3 p3);\n\n    default TriFunction<P1, P2, P3, R> andThen(Function<? super R, ? extends R> after) {\n        Objects.requireNonNull(after);\n        return (p1, p2, p3) -> after.apply(apply(p1, p2, p3));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/item/IItemHooks.java",
    "content": "package net.darkhax.bookshelf.common.api.item;\n\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.function.Consumer;\n\npublic interface IItemHooks {\n\n    default void addCreativeTabForms(CreativeModeTab tab, Consumer<ItemStack> displayItems) {\n        // NO-OP\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/loot/LootPoolEntryDescriber.java",
    "content": "package net.darkhax.bookshelf.common.api.loot;\n\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Consumer;\n\n/**\n * Describes the potential items that a loot pool entry can generate. See {@link LootPoolEntryDescriptions} for usage.\n */\n@FunctionalInterface\npublic interface LootPoolEntryDescriber {\n\n    /**\n     * Describes items that may potentially be dropped by a loot pool entry.\n     *\n     * @param registries Access to the current game registries.\n     * @param entry      The loot pool entry to be processed.\n     * @param collector  Collects entries from the entry into the desired format.\n     */\n    void getPotentialDrops(@NotNull RegistryAccess registries, @NotNull LootPoolEntryContainer entry, Consumer<ItemStack> collector);\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/loot/LootPoolEntryDescriptions.java",
    "content": "package net.darkhax.bookshelf.common.api.loot;\n\nimport com.mojang.datafixers.util.Either;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorCompositeEntryBase;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootItem;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootPool;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorNestedLootTable;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorTagEntry;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.item.component.ItemLore;\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport net.minecraft.world.level.storage.loot.entries.DynamicLoot;\nimport net.minecraft.world.level.storage.loot.entries.EmptyLootItem;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * Provides a system for describing what items can be dropped by a loot table.\n */\npublic class LootPoolEntryDescriptions {\n\n    private static final CachedSupplier<ItemStack> UNKNOWN_ITEM_DISPLAY = CachedSupplier.cache(() -> {\n        final ItemStack stack = new ItemStack(Items.STRUCTURE_VOID);\n        stack.set(DataComponents.ITEM_NAME, Component.translatable(\"tooltips.bookshelf.loot.unknown\"));\n        stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable(\"tooltips.bookshelf.loot.unknown.desc\").withStyle(ChatFormatting.GRAY))));\n        return stack;\n    });\n\n    private static final CachedSupplier<ItemStack> EMPTY_ITEM_DISPLAY = CachedSupplier.cache(() -> {\n        final ItemStack stack = new ItemStack(Items.BARRIER);\n        stack.set(DataComponents.ITEM_NAME, Component.translatable(\"tooltips.bookshelf.loot.empty\"));\n        stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable(\"tooltips.bookshelf.loot.empty.desc\").withStyle(ChatFormatting.GRAY))));\n        return stack;\n    });\n\n    private static final CachedSupplier<ItemStack> DYNAMIC_DISPLAY = CachedSupplier.cache(() -> {\n        final ItemStack stack = new ItemStack(Items.JIGSAW);\n        stack.set(DataComponents.ITEM_NAME, Component.translatable(\"tooltips.bookshelf.loot.dynamic\"));\n        stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable(\"tooltips.bookshelf.loot.dynamic.desc\").withStyle(ChatFormatting.GRAY))));\n        return stack;\n    });\n\n    private static final Map<LootPoolEntryType, LootPoolEntryDescriber> DESCRIBERS = new HashMap<>();\n    private static boolean hasInitialized = false;\n\n    public static final LootPoolEntryDescriber EMPTY = (server, entry, collector) -> {\n        if (entry instanceof EmptyLootItem) {\n            collector.accept(EMPTY_ITEM_DISPLAY.get());\n        }\n    };\n\n    public static final LootPoolEntryDescriber ITEM = (server, entry, collector) -> {\n        if (entry instanceof AccessorLootItem accessor) {\n            collector.accept(new ItemStack(accessor.bookshelf$item()));\n        }\n    };\n\n    public static final LootPoolEntryDescriber LOOT_TABLE = (server, entry, collector) -> {\n        if (entry instanceof AccessorNestedLootTable accessor) {\n            getPotentialItems(server, accessor.bookshelf$contents(), collector);\n        }\n    };\n    public static final LootPoolEntryDescriber DYNAMIC = (server, entry, collector) -> {\n        if (entry instanceof DynamicLoot) {\n            collector.accept(DYNAMIC_DISPLAY.get());\n        }\n    };\n    public static final LootPoolEntryDescriber TAG = (server, entry, collector) -> {\n        if (entry instanceof AccessorTagEntry tagEntry) {\n            getTagItems(tagEntry.bookshelf$tag(), collector);\n        }\n    };\n    public static final LootPoolEntryDescriber COMPOSITE = (server, entry, collector) -> {\n        if (entry instanceof AccessorCompositeEntryBase accessor) {\n            getPotentialItems(server, accessor.bookshelf$children(), collector);\n        }\n    };\n    public static final LootPoolEntryDescriber ITEM_STACK = (server, entry, collector) -> {\n        if (entry instanceof LootItemStack loot) {\n            collector.accept(loot.getBaseStack());\n        }\n    };\n\n    private static void bootstrap() {\n        if (!hasInitialized) {\n            final LootDescriptionAdapter register = new LootDescriptionAdapter(DESCRIBERS::put);\n            Services.CONTENT.get().forEach(provider -> provider.defineLootDescriptions(register));\n            hasInitialized = true;\n        }\n    }\n\n    /**\n     * Generates a list of unique items that can generate from a loot table.\n     *\n     * @param registries The current registries.\n     * @param table      The loot table to examine.\n     * @return A list containing the entries.\n     */\n    public static List<ItemStack> getUniqueItems(@NotNull RegistryAccess registries, LootTable table) {\n        final List<ItemStack> items = new ArrayList<>();\n        getPotentialItems(registries, table, stack -> addStacking(items, stack));\n        return items;\n    }\n\n    /**\n     * Gets potential drops for a loot table.\n     *\n     * @param registries The current registries.\n     * @param table      The loot table to examine.\n     * @param consumer   Collects entries in your desired format.\n     */\n    public static void getPotentialItems(@NotNull RegistryAccess registries, Either<ResourceKey<LootTable>, LootTable> table, Consumer<ItemStack> consumer) {\n        final LootTable resolved = table.map(rl -> registries.registryOrThrow(Registries.LOOT_TABLE).get(rl), Function.identity());\n        if (resolved != null) {\n            getPotentialItems(registries, resolved, consumer);\n        }\n    }\n\n    /**\n     * Gets potential drops for a loot table.\n     *\n     * @param registries The current registries.\n     * @param table      The loot table to examine.\n     * @param consumer   Collects entries in your desired format.\n     */\n    public static void getPotentialItems(@NotNull RegistryAccess registries, LootTable table, Consumer<ItemStack> consumer) {\n        if (table instanceof AccessorLootTable tableAccess) {\n            for (LootPool pool : tableAccess.bookshelf$pools()) {\n                if (pool instanceof AccessorLootPool poolAccess) {\n                    getPotentialItems(registries, poolAccess.bookshelf$entries(), consumer);\n                }\n            }\n        }\n    }\n\n    /**\n     * Gets potential drops for a list of loot pool entries.\n     *\n     * @param registries The current registries.\n     * @param entries    A list of entries to examine.\n     * @param collector  Collects entries in your desired format.\n     */\n    public static void getPotentialItems(@NotNull RegistryAccess registries, List<LootPoolEntryContainer> entries, Consumer<ItemStack> collector) {\n        for (LootPoolEntryContainer entry : entries) {\n            getPotentialItems(registries, entry, collector);\n        }\n    }\n\n    /**\n     * Gets potential drops from a loot pool entry.\n     *\n     * @param registries The current registries.\n     * @param entry      The pool entry to examine.\n     * @param collector  Collects entries in your desired format.\n     */\n    public static void getPotentialItems(@NotNull RegistryAccess registries, LootPoolEntryContainer entry, Consumer<ItemStack> collector) {\n        bootstrap();\n        final LootPoolEntryDescriber describer = DESCRIBERS.get(entry.getType());\n        if (describer != null) {\n            describer.getPotentialDrops(registries, entry, collector);\n        }\n        else {\n            collector.accept(UNKNOWN_ITEM_DISPLAY.get());\n        }\n    }\n\n    /**\n     * Adds an ItemStack to a list, only if the item does not stack with any of the items already in the list.\n     *\n     * @param items The list to add to.\n     * @param toAdd The entry to add.\n     */\n    private static void addStacking(List<ItemStack> items, ItemStack toAdd) {\n        for (ItemStack existing : items) {\n            if (Objects.equals(existing, toAdd) || ItemStack.isSameItemSameComponents(existing, toAdd)) {\n                return;\n            }\n        }\n        items.add(toAdd);\n    }\n\n    private static void getTagItems(TagKey<Item> tag, Consumer<ItemStack> collector) {\n        for (Holder<Item> item : BuiltInRegistries.ITEM.getTagOrEmpty(tag)) {\n            collector.accept(new ItemStack(item));\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/menu/data/BlockPosData.java",
    "content": "package net.darkhax.bookshelf.common.api.menu.data;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.inventory.ContainerData;\n\nimport java.util.Arrays;\n\n/**\n * Allows a block position to be kept in sync using the container system. The server should always construct this\n * directly while the client should use SimpleContainerData with size of 3.\n */\npublic class BlockPosData implements ContainerData {\n\n    private final int[] pos;\n    private final boolean mutable;\n\n    public BlockPosData(BlockPos pos) {\n        this(pos, false);\n    }\n\n    public BlockPosData(BlockPos pos, boolean mutable) {\n        this.pos = new int[]{pos.getX(), pos.getY(), pos.getZ()};\n        this.mutable = mutable;\n    }\n\n    @Override\n    public int get(int slot) {\n        return pos[slot];\n    }\n\n    @Override\n    public void set(int slot, int value) {\n        if (this.mutable) {\n            pos[slot] = value;\n        }\n    }\n\n    @Override\n    public int getCount() {\n        return 3;\n    }\n\n    /**\n     * Gets the BlockPos currently held by the container data. This should only be called on the server.\n     *\n     * @return The BlockPos being held.\n     */\n    public BlockPos getPos() {\n        return new BlockPos(pos[0], pos[1], pos[2]);\n    }\n\n    /**\n     * Reads a BlockPos from untyped container data. This should be used to read the position on the client which should\n     * be using a SimpleContainerData and not a BlockPosData.\n     *\n     * @param data The data to read from.\n     * @return The BlockPos that was ready.\n     */\n    public static BlockPos readPos(ContainerData data) {\n        if (data.getCount() != 3) {\n            throw new IllegalStateException(\"Can not read BlockPos from container data. Expected 3 values, found \" + data.getCount() + \". data=\" + Arrays.toString(toArray(data)));\n        }\n        return new BlockPos(data.get(0), data.get(1), data.get(2));\n    }\n\n    private static int[] toArray(ContainerData containerData) {\n        final int[] data = new int[containerData.getCount()];\n        for (int i = 0; i < containerData.getCount(); i++) {\n            data[i] = containerData.get(i);\n        }\n        return data;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/menu/slot/InputSlot.java",
    "content": "package net.darkhax.bookshelf.common.api.menu.slot;\n\nimport com.mojang.datafixers.util.Pair;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.Container;\nimport net.minecraft.world.inventory.InventoryMenu;\nimport net.minecraft.world.inventory.Slot;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.Predicate;\n\n/**\n * A basic input slot implementation.\n */\npublic class InputSlot extends Slot {\n\n    private final ResourceLocation emptyTexture;\n    private final Predicate<ItemStack> canPlace;\n    \n    public InputSlot(Container container, int slot, int x, int y, ResourceLocation emptyTexture) {\n        this(container, slot, x, y, emptyTexture, stack -> true);\n    }\n\n    public InputSlot(Container container, int slot, int x, int y, ResourceLocation emptyTexture, Predicate<ItemStack> canPlace) {\n        super(container, slot, x, y);\n        this.emptyTexture = emptyTexture;\n        this.canPlace = canPlace;\n    }\n\n    @Override\n    public int getMaxStackSize() {\n        return 1;\n    }\n\n    @Nullable\n    @Override\n    public Pair<ResourceLocation, ResourceLocation> getNoItemIcon() {\n        return Pair.of(InventoryMenu.BLOCK_ATLAS, this.emptyTexture);\n    }\n\n    @Override\n    public boolean mayPlace(@NotNull ItemStack stack) {\n        return this.canPlace.test(stack);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/menu/slot/OutputSlot.java",
    "content": "package net.darkhax.bookshelf.common.api.menu.slot;\n\nimport net.minecraft.world.Container;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.inventory.Slot;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BiConsumer;\n\n/**\n * A basic output slot implementation.\n */\npublic class OutputSlot extends Slot {\n\n    @Nullable\n    private final BiConsumer<Player, ItemStack> takeFunc;\n\n    public OutputSlot(Container potContainer, int slot, int x, int y) {\n        this(potContainer, slot, x, y, null);\n    }\n\n    public OutputSlot(Container potContainer, int slot, int x, int y, @Nullable BiConsumer<Player, ItemStack> takeFunc) {\n        super(potContainer, slot, x, y);\n        this.takeFunc = takeFunc;\n    }\n\n    @Override\n    public boolean mayPlace(@NotNull ItemStack stack) {\n        return false;\n    }\n\n    @Override\n    public void onTake(@NotNull Player player, @NotNull ItemStack stack) {\n        super.onTake(player, stack);\n        if (this.takeFunc != null) {\n            this.takeFunc.accept(player, stack);\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/network/AbstractPacket.java",
    "content": "package net.darkhax.bookshelf.common.api.network;\n\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * A basic packet implementation.\n *\n * @param <T> The type of the payload.\n */\npublic abstract class AbstractPacket<T extends CustomPacketPayload> implements IPacket<T> {\n\n    /**\n     * The type of the payload.\n     */\n    private final CustomPacketPayload.Type<T> type;\n\n    /**\n     * A codec to serialize the payload.\n     */\n    private final StreamCodec<RegistryFriendlyByteBuf, T> codec;\n\n    /**\n     * The intended destination of the payload.\n     */\n    private final Destination direction;\n\n    /**\n     * A packet that is sent from the server to the client.\n     *\n     * @param id    The packet ID.\n     * @param codec The payload codec.\n     */\n    public AbstractPacket(ResourceLocation id, StreamCodec<RegistryFriendlyByteBuf, T> codec) {\n        this(id, codec, Destination.SERVER_TO_CLIENT);\n    }\n\n    /**\n     * A simple packet type.\n     *\n     * @param id        The packet ID.\n     * @param codec     The payload codec.\n     * @param direction The intended destination of the packet.\n     */\n    public AbstractPacket(ResourceLocation id, StreamCodec<RegistryFriendlyByteBuf, T> codec, Destination direction) {\n        this.type = new CustomPacketPayload.Type<>(id);\n        this.codec = codec;\n        this.direction = direction;\n    }\n\n    @Override\n    public CustomPacketPayload.Type<T> type() {\n        return this.type;\n    }\n\n    @Override\n    public StreamCodec<RegistryFriendlyByteBuf, T> streamCodec() {\n        return this.codec;\n    }\n\n    @Override\n    public Destination destination() {\n        return this.direction;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/network/Destination.java",
    "content": "package net.darkhax.bookshelf.common.api.network;\n\n/**\n * Determines where the packet will be resolved.\n */\npublic enum Destination {\n\n    /**\n     * Describes a situation where the server has a payload that will be sent to a client. The payload will be handled\n     * on the client and can use code that is only available on a dedicated client.\n     */\n    SERVER_TO_CLIENT,\n\n    /**\n     * Describes a situation where the client has a payload that will be sent to a server. The payload will be handled\n     * by the server and can access the game state. Please keep in mind that payloads originating on the client can be\n     * forged and should not be trusted without an appropriate level of validation on the server.\n     */\n    CLIENT_TO_SERVER,\n\n    /**\n     * Describes a situation where the packet can originate from and be handled by a client or a server. These packets\n     * have the limitations of both SERVER_TOL_CLIENT and CLIENT_TO_SERVER packets.\n     */\n    BIDIRECTIONAL;\n\n    /**\n     * Checks if the payload can be handled on a server.\n     *\n     * @return If the payload can be handled by a server.\n     */\n    public boolean handledByServer() {\n        return this == CLIENT_TO_SERVER || this == BIDIRECTIONAL;\n    }\n\n    /**\n     * Checks if the payload can be handled on a client.\n     *\n     * @return If the payload can be handled by a client.\n     */\n    public boolean handledByClient() {\n        return this == SERVER_TO_CLIENT || this == BIDIRECTIONAL;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/network/INetworkHandler.java",
    "content": "package net.darkhax.bookshelf.common.api.network;\n\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerPlayer;\n\n/**\n * Provides platform specific implementations of network related code. Access using\n * {@link net.darkhax.bookshelf.common.api.service.Services#NETWORK}.\n */\npublic interface INetworkHandler {\n\n    /**\n     * Registers a Bookshelf packet type to the packet registry.\n     *\n     * @param packet The packet type to register.\n     * @param <T>    The type of the payload.\n     */\n    <T extends CustomPacketPayload> void register(IPacket<T> packet);\n\n    /**\n     * Sends a payload from the client to the server.\n     *\n     * @param payload The payload to send.\n     * @param <T>     The type of the payload.\n     */\n    <T extends CustomPacketPayload> void sendToServer(T payload);\n\n    /**\n     * Sends a packet from the server to a player.\n     *\n     * @param recipient The recipient of the packet.\n     * @param payload   The payload to send.\n     * @param <T>       The type of the payload.\n     */\n    <T extends CustomPacketPayload> void sendToPlayer(ServerPlayer recipient, T payload);\n\n    /**\n     * Tests if a payload type can be sent to a player.\n     *\n     * @param recipient The recipient of the packet.\n     * @param payload   The payload to test.\n     * @return If the payload can be sent to the recipient player.\n     */\n    default boolean canSendPacket(ServerPlayer recipient, CustomPacketPayload payload) {\n        return this.canSendPacket(recipient, payload.type().id());\n    }\n\n    /**\n     * Tests if a payload type can be sent to a player.\n     *\n     * @param recipient The recipient of the packet.\n     * @param packet    The packet to test.\n     * @return If the payload can be sent to the recipient player.\n     */\n    default boolean canSendPacket(ServerPlayer recipient, IPacket<?> packet) {\n        return this.canSendPacket(recipient, packet.type().id());\n    }\n\n    /**\n     * Tests if a payload type can be sent to a player.\n     *\n     * @param recipient The recipient of the packet.\n     * @param payloadId The payload type ID.\n     * @return If the payload can be sent to the recipient player.\n     */\n    boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId);\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/network/IPacket.java",
    "content": "package net.darkhax.bookshelf.common.api.network;\n\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\nimport net.darkhax.bookshelf.common.api.annotation.OnlyFor;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.server.players.PlayerList;\nimport org.jetbrains.annotations.Nullable;\n\n/**\n * Defines a custom payload packet. These packets must be registered using an\n * {@link net.darkhax.bookshelf.common.api.registry.ContentProvider}.\n *\n * @param <T> The type of the payload.\n */\npublic interface IPacket<T extends CustomPacketPayload> {\n\n    /**\n     * Gets the payload type.\n     *\n     * @return The payload type.\n     */\n    CustomPacketPayload.Type<T> type();\n\n    /**\n     * Gets a stream codec that can serialize the payload across the network.\n     *\n     * @return The stream coded used to serialize the payload.\n     */\n    StreamCodec<RegistryFriendlyByteBuf, T> streamCodec();\n\n    /**\n     * Defines how the packet is meant to be sent and where it should be handled.\n     *\n     * @return The intended destination of the packet.\n     */\n    Destination destination();\n\n    /**\n     * This method will be called when the custom payload is received. This method can be called on both the client and\n     * server, depending on the destination type defined by {@link #destination()}.\n     *\n     * @param sender   The sender of the packet. This will always be null for packets handled on the client.\n     * @param isServer True when the packet is being handled on the server.\n     * @param payload  The payload that was received.\n     */\n    void handle(@Nullable ServerPlayer sender, boolean isServer, T payload);\n\n    /**\n     * Sends the packet from the server to a specific player.\n     *\n     * @param recipient The intended recipient of the payload.\n     * @param payload   The payload to send.\n     */\n    default void toPlayer(ServerPlayer recipient, T payload) {\n        if (!this.destination().handledByClient()) {\n            Constants.LOG.error(\"Attempted to send invalid packet {} to client! Class: {} Destination: {} Payload: {}\", this.type().id(), this.getClass(), this.destination(), payload.toString());\n            throw new IllegalStateException(\"Attempted to send invalid packet \" + this.type().id() + \" to client!\");\n        }\n        Services.NETWORK.sendToPlayer(recipient, payload);\n    }\n\n    /**\n     * Sends the packet from the server to all connected players.\n     *\n     * @param level   A serverside level, used to access the player list.\n     * @param payload The payload to send.\n     */\n    default void toAllPlayers(ServerLevel level, T payload) {\n        toAllPlayers(level.getServer(), payload);\n    }\n\n    /**\n     * Sends the packet from the server to all connected players.\n     *\n     * @param server  The server instance, used to access the player list.\n     * @param payload The payload to send.\n     */\n    default void toAllPlayers(MinecraftServer server, T payload) {\n        toAllPlayers(server.getPlayerList(), payload);\n    }\n\n    /**\n     * Sends the packet from the server to all connected players.\n     *\n     * @param playerList The player list.\n     * @param payload    The payload to send.\n     */\n    default void toAllPlayers(PlayerList playerList, T payload) {\n        for (ServerPlayer player : playerList.getPlayers()) {\n            toPlayer(player, payload);\n        }\n    }\n\n    /**\n     * Sends a packet from a client to the server.\n     *\n     * @param payload The payload to send.\n     */\n    @OnlyFor(PhysicalSide.CLIENT)\n    default void toServer(T payload) {\n        if (!this.destination().handledByServer()) {\n            Constants.LOG.error(\"Attempted to send invalid packet {} to server! Class: {} Destination: {} Payload: {}\", this.type().id(), this.getClass(), this.destination(), payload.toString());\n            throw new IllegalStateException(\"Attempted to send invalid packet \" + this.type().id() + \" to server!\");\n        }\n        if (Minecraft.getInstance().getConnection() == null) {\n            Constants.LOG.error(\"Attempted to send packet {} before a connection to a server has been established!\", this.type().id());\n            throw new IllegalStateException(\"Attempted to send packet \" + this.type().id() + \" before being connected to a server!\");\n        }\n        Services.NETWORK.sendToServer(payload);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/ContentProvider.java",
    "content": "package net.darkhax.bookshelf.common.api.registry;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\nimport net.darkhax.bookshelf.common.api.annotation.OnlyFor;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootPoolAdditionAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter;\nimport net.minecraft.advancements.CriterionTrigger;\nimport net.minecraft.advancements.critereon.ItemSubPredicate;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.core.component.DataComponentType;\nimport net.minecraft.world.effect.MobEffect;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.ai.attributes.Attribute;\nimport net.minecraft.world.entity.animal.CatVariant;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.alchemy.Potion;\nimport net.minecraft.world.item.alchemy.PotionBrewing;\nimport net.minecraft.world.item.crafting.RecipeSerializer;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunctionType;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;\n\n/**\n * An interface for adding custom game content such as blocks and items during the appropriate stages of the game's\n * lifecycle.\n * <p>\n * Implementations of this interface are discovered automatically by Bookshelf using the {@link java.util.ServiceLoader}\n * mechanism. To make your provider loadable, add the fully qualified name of your implementation to the file:\n * <pre>\n *     META-INF/services/net.darkhax.bookshelf.common.api.registry.ContentProvider\n * </pre>\n */\npublic interface ContentProvider {\n\n    /**\n     * Registers new attributes with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineAttributes(GameRegistryAdapter<Attribute> registry) {\n    }\n\n    /**\n     * Registers new blocks with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineBlocks(BlockRegistryAdapter registry) {\n    }\n\n    /**\n     * Registers new block entities with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineBlockEntities(GameRegistryAdapter<BlockEntityType<?>> registry) {\n    }\n\n    /**\n     * Registers new items with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineItems(GameRegistryAdapter<Item> registry) {\n    }\n\n    /**\n     * Registers new recipe types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineRecipeTypes(RecipeTypeAdapter registry) {\n    }\n\n    /**\n     * Registers new creative mode tabs with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineCreativeTabs(CreativeModeTabAdapter registry) {\n    }\n\n    /**\n     * Registers new command argument types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineCommandArguments(CommandArgumentAdapter registry) {\n    }\n\n    /**\n     * Registers new commands with the game. This may happen multiple times per game instance depending on user\n     * actions.\n     *\n     * @param dispatcher The command dispatcher to populate with your new commands.\n     * @param context    Context used to build commands.\n     * @param selection  The type of commands that should be registered.\n     */\n    default void defineCommands(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext context, Commands.CommandSelection selection) {\n    }\n\n    /**\n     * Registers new ingredient types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineIngredientTypes(IngredientTypeAdapter registry) {\n    }\n\n    /**\n     * Adds new trades to the villager trade pools.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineTrades(VillagerTradeAdapter registry) {\n    }\n\n    /**\n     * Registers new mob effects with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineMobEffects(GameRegistryAdapter<MobEffect> registry) {\n    }\n\n    /**\n     * Registers new criteria triggers with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineCriteriaTriggers(GameRegistryAdapter<CriterionTrigger<?>> registry) {\n    }\n\n    /**\n     * Registers an item predicate type with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineItemSubPredicates(GameRegistryAdapter<ItemSubPredicate.Type<?>> registry) {\n    }\n\n    /**\n     * Registers entity types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineEntities(GameRegistryAdapter<EntityType<?>> registry) {\n    }\n\n    /**\n     * Registers cat variants with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineCatVariants(GameRegistryAdapter<CatVariant> registry) {\n    }\n\n    /**\n     * Registers potions with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void definePotions(GameRegistryAdapter<Potion> registry) {\n    }\n\n    /**\n     * Registers new potion brewing recipes with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineBrews(PotionBrewing.Builder registry) {\n    }\n\n    /**\n     * Registers new decorated pot patterns with the game, and create associations between items and patterns.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void definePotPatterns(PotPatternAdapter registry) {\n    }\n\n    /**\n     * Registers new item components with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineItemComponents(GameRegistryAdapter<DataComponentType<?>> registry) {\n    }\n\n    /**\n     * Registers new enchantment components with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineEnchantmentComponents(GameRegistryAdapter<DataComponentType<?>> registry) {\n    }\n\n    /**\n     * Registers new loot conditions with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineLootConditions(GameRegistryAdapter<LootItemConditionType> registry) {\n    }\n\n    /**\n     * Registers new loot functions with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineLootFunctions(GameRegistryAdapter<LootItemFunctionType<?>> registry) {\n    }\n\n    /**\n     * Registers new recipe serializers with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineRecipeSerializers(GameRegistryAdapter<RecipeSerializer<?>> registry) {\n    }\n\n    /**\n     * Registers new loot entry types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineLootEntryTypes(LootEntryTypeAdapter registry) {\n    }\n\n    /**\n     * Inject entries into existing loot pools. For example, this can be used to add new loot to the dungeon loot\n     * chest.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineLootPoolAdditions(LootPoolAdditionAdapter registry) {\n    }\n\n    /**\n     * Registers a new descriptor for loot entries.\n     *\n     * @param registry Accepts registry requests.\n     */\n    default void defineLootDescriptions(LootDescriptionAdapter registry) {\n    }\n\n    /**\n     * Registers a new bookshelf load condition for JSON resources.\n     *\n     * @param registry Accepts registry requests.\n     */\n    default void defineLoadConditions(GenericRegistryAdapter<MapCodec<? extends ILoadCondition>> registry) {\n    }\n\n    /**\n     * Registers new menu types with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineMenuType(MenuTypeAdapter registry) {\n    }\n\n    /**\n     * Registers new packets with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void definePackets(PacketAdapter registry) {\n    }\n\n    /**\n     * Registers new sound events with the game.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    default void defineSounds(SoundEventAdapter registry) {\n\n    }\n\n    /**\n     * Associates menu types with screens.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    @OnlyFor(PhysicalSide.CLIENT)\n    default void defineMenuScreens(MenuScreenAdapter registry) {\n    }\n\n    /**\n     * Associates blocks with a render type. For now, NeoForge also requires this to be defined in the model file.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    @OnlyFor(PhysicalSide.CLIENT)\n    default void defineBlockRenderTypes(BlockRenderTypeAdapter registry) {\n    }\n\n    /**\n     * Associates a block entity with a block entity renderer.\n     *\n     * @param registry Adapts registry requests to the current mod loader.\n     */\n    @OnlyFor(PhysicalSide.CLIENT)\n    default void defineBlockRenderers(BlockEntityRendererAdapter registry) {\n    }\n\n    /**\n     * Gets the namespace that all content from the provider should be registered under. This MUST be the same modid\n     * that is used by your NeoForge/Fabric mod.\n     *\n     * @return The namespace to register content with.\n     */\n    String namespace();\n\n    /**\n     * Checks if content from the provider should be loaded or not. All providers will be loaded by default, however\n     * custom implementations may have additional requirements.\n     * <p>\n     * Bookshelf will still classload your provider if this returns false, this only prevents content from being loaded.\n     * It is the implementers responsibility to ensure their class can be classloaded safely.\n     *\n     * @return If content from this provider should be loaded.\n     */\n    default boolean canLoad() {\n        return true;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/RegistrationContext.java",
    "content": "package net.darkhax.bookshelf.common.api.registry;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.entity.DecoratedPotPattern;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * Holds context that is shared between different registry adapters.\n */\npublic final class RegistrationContext {\n\n    private final String namespace;\n    private final Map<RegistryReference<ResourceKey<Block>, Block>, Function<Block, Item>> placeableBlocks = new HashMap<>();\n\n    private static final Map<Item, ResourceKey<DecoratedPotPattern>> INTERNAL_POT_PATTERN_ITEMS = new HashMap<>();\n    public static final Map<Item, ResourceKey<DecoratedPotPattern>> POT_PATTERN_ITEMS = Collections.unmodifiableMap(INTERNAL_POT_PATTERN_ITEMS);\n\n    public RegistrationContext(String namespace) {\n        this.namespace = namespace;\n    }\n\n    /**\n     * Gets the namespace that all new content should be registered with.\n     *\n     * @return The namespace new content is registered with.\n     */\n    public String namespace() {\n        return this.namespace;\n    }\n\n    /**\n     * Associates a block with a factory that provides its corresponding item form. The produced item will be\n     * automatically registered with the same ID as the block.\n     *\n     * @param block     The block to associate the item with.\n     * @param itemBlock A factory that creates the placer item.\n     */\n    public void addPlaceableBlock(RegistryReference<ResourceKey<Block>, Block> block, Function<Block, Item> itemBlock) {\n        this.placeableBlocks.put(block, itemBlock);\n    }\n\n    /**\n     * Provides an unmodifiable view of placeable blocks and their associated item factories.\n     *\n     * @return An unmodifiable map of placeable blocks to their placer item factories.\n     */\n    public Map<RegistryReference<ResourceKey<Block>, Block>, Function<Block, Item>> getPlaceableBlocks() {\n        return Collections.unmodifiableMap(this.placeableBlocks);\n    }\n\n    /**\n     * Associates an item with a decorated pot pattern. Replacing existing associations is not a supported use case.\n     *\n     * @param item    The item to associate with the pattern.\n     * @param pattern The pattern displayed by the item.\n     */\n    public void addPotPatternItem(Item item, ResourceKey<DecoratedPotPattern> pattern) {\n        if (INTERNAL_POT_PATTERN_ITEMS.containsKey(item)) {\n            Constants.LOG.warn(\"Mod {} has changed the pot pattern of {} to {} from {}.\", this.namespace(), BuiltInRegistries.ITEM.getKey(item), pattern.location(), INTERNAL_POT_PATTERN_ITEMS.get(item).location());\n        }\n        INTERNAL_POT_PATTERN_ITEMS.put(item, pattern);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/RegistryReference.java",
    "content": "package net.darkhax.bookshelf.common.api.registry;\n\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * Represents an entry in a game registry.\n *\n * @param key   The key the value was registered with.\n * @param value A supplier that produces the registered value.\n * @param <K>   The type of the registry key.\n * @param <V>   The type of the registered value.\n */\npublic record RegistryReference<K, V>(K key, CachedSupplier<V> value) {\n\n    /**\n     * A helper method that produces a reference for a registry that uses ResourceLocation based keys.\n     *\n     * @param key   The key the value was registered with.\n     * @param value A supplier that produces the registered value.\n     * @param <V>   The type of the registered value.\n     * @return A reference to the registry entry.\n     */\n    public static <V> RegistryReference<ResourceLocation, V> location(ResourceLocation key, CachedSupplier<V> value) {\n        return new RegistryReference<>(key, value);\n    }\n\n    /**\n     * A helper method that produces a reference for a registry that uses ResourceKey.\n     *\n     * @param key The key to lookup.\n     * @param <V> The type of the value held in the registry.\n     * @return A reference to a value in a registry.\n     */\n    public static <V> RegistryReference<ResourceKey<V>, V> resource(ResourceKey<V> key) {\n        return new RegistryReference<>(key, CachedSupplier.of(key));\n    }\n\n    /**\n     * A helper method that produces a reference for a registry that uses ResourceKey.\n     *\n     * @param registryKey The key for the registry the value is registered in.\n     * @param key         The key the value was registered with.\n     * @param value       A supplier that produces the registered value.\n     * @param <V>         The type of the registered value.\n     * @return A reference to the registry entry.\n     */\n    public static <V> RegistryReference<ResourceKey<V>, V> resource(ResourceKey<? extends Registry<V>> registryKey, ResourceLocation key, CachedSupplier<V> value) {\n        return new RegistryReference<>(ResourceKey.create(registryKey, key), value);\n    }\n\n    /**\n     * A helper method that produces a reference for a registry that uses ResourceKey.\n     *\n     * @param registry The registry the value is registered in.\n     * @param key      The key the value was registered with.\n     * @param value    A supplier that produces the registered value.\n     * @param <V>      The type of the registered value.\n     * @return A reference to the registry entry.\n     */\n    public static <V> RegistryReference<ResourceKey<V>, V> resource(Registry<V> registry, ResourceLocation key, CachedSupplier<V> value) {\n        return resource(registry.key(), key, value);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/GameRegistryAdapter.java",
    "content": "package net.darkhax.bookshelf.common.api.registry.adapters;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\n/**\n * A basic registry adapter that can register into most vanilla style registries.\n *\n * @param <V> The type of value held by the registry.\n */\npublic class GameRegistryAdapter<V> implements RegistryAdapter<ResourceKey<V>, V> {\n\n    /**\n     * Context that is shared by all registry adapters owned by the same namespace.\n     */\n    protected final RegistrationContext context;\n\n    /**\n     * The id of the registry being adapted.\n     */\n    protected final ResourceKey<Registry<V>> registryKey;\n\n    /**\n     * A function that accepts and registers a key and value supplier.\n     */\n    protected final BiConsumer<ResourceKey<V>, Supplier<V>> registryFunc;\n\n    public GameRegistryAdapter(RegistrationContext context, ResourceKey<Registry<V>> registryKey, BiConsumer<ResourceKey<V>, Supplier<V>> registryFunc) {\n        this.context = context;\n        this.registryKey = registryKey;\n        this.registryFunc = registryFunc;\n    }\n\n    @Override\n    public RegistryReference<ResourceKey<V>, V> add(String key, Supplier<V> value) {\n        final ResourceKey<V> resourceKey = ResourceKey.create(registryKey, ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key));\n        this.registryFunc.accept(resourceKey, value);\n        return RegistryReference.resource(resourceKey);\n    }\n\n    /**\n     * Creates a new ResourceLocation using the current namespace.\n     *\n     * @param key The path of the ResourceLocation.\n     * @return A new ResourceLocation with the current namespace.\n     */\n    public final ResourceLocation id(String key) {\n        return ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/GenericRegistryAdapter.java",
    "content": "package net.darkhax.bookshelf.common.api.registry.adapters;\n\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\n/**\n * A basic registry adapter that can register into registries that are not standard vanilla registries.\n *\n * @param <V> The type of value held by the registry.\n */\npublic class GenericRegistryAdapter<V> implements RegistryAdapter<ResourceLocation, V> {\n\n    /**\n     * Context that is shared by all registry adapters owned by the same namespace.\n     */\n    protected final RegistrationContext context;\n\n    /**\n     * A function that accepts and registers a key and value supplier.\n     */\n    protected final BiConsumer<ResourceLocation, Supplier<V>> registryFunc;\n\n    public GenericRegistryAdapter(RegistrationContext context, BiConsumer<ResourceLocation, Supplier<V>> registryFunc) {\n        this.context = context;\n        this.registryFunc = registryFunc;\n    }\n\n    /**\n     * Adds a value to the registry. Values are not necessarily registered immediately.\n     *\n     * @param id    The ID to register the value under.\n     * @param value A supplier that produces the value to register.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceLocation, V> add(ResourceLocation id, Supplier<V> value) {\n        final CachedSupplier<V> cache = CachedSupplier.cache(value);\n        this.registryFunc.accept(id, cache);\n        return RegistryReference.location(id, cache);\n    }\n\n    /**\n     * Adds a value to the registry. Values are not necessarily registered immediately.\n     *\n     * @param id    The ID to register the value under.\n     * @param value The value to register.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceLocation, V> add(ResourceLocation id, V value) {\n        return this.add(id, () -> value);\n    }\n\n    @Override\n    public RegistryReference<ResourceLocation, V> add(String key, Supplier<V> value) {\n        return this.add(ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key), value);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/RegistryAdapter.java",
    "content": "package net.darkhax.bookshelf.common.api.registry.adapters;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\n\nimport java.util.function.Supplier;\n\n/**\n * Provides a loader agnostic interface for registering content.\n *\n * @param <K> The type of key used by the registry.\n * @param <V> The type of value being registered.\n */\npublic interface RegistryAdapter<K, V> {\n\n    /**\n     * Adds a value to the registry. Values are not necessarily registered immediately.\n     *\n     * @param key   The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param value The value to register.\n     * @return A reference to the registry entry.\n     */\n    default RegistryReference<K, V> add(String key, V value) {\n        return this.add(key, () -> value);\n    }\n\n    /**\n     * Adds a value to the registry. Values are not necessarily registered immediately.\n     *\n     * @param key   The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param value A supplier that produces the value to register.\n     * @return A reference to the registry entry.\n     */\n    RegistryReference<K, V> add(String key, Supplier<V> value);\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/service/Services.java",
    "content": "package net.darkhax.bookshelf.common.api.service;\n\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.network.INetworkHandler;\nimport net.darkhax.bookshelf.common.api.registry.ContentProvider;\nimport net.darkhax.bookshelf.common.api.util.IGameplayHelper;\nimport net.darkhax.bookshelf.common.api.util.IPlatformHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.ServiceLoader;\nimport java.util.stream.Collectors;\n\npublic class Services {\n\n    public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class);\n    public static final CachedSupplier<List<ContentProvider>> CONTENT = CachedSupplier.cache(() -> loadMany(ContentProvider.class));\n    public static final IGameplayHelper GAMEPLAY = load(IGameplayHelper.class);\n    public static final INetworkHandler NETWORK = load(INetworkHandler.class);\n\n    public static <T> T load(Class<T> clazz) {\n        final T loadedService = ServiceLoader.load(clazz).findFirst().orElseThrow(() -> new NullPointerException(\"Failed to load service for \" + clazz.getName()));\n        Constants.LOG.debug(\"Loaded {} for service {}.\", loadedService, clazz);\n        return loadedService;\n    }\n\n    public static <T> List<T> loadMany(Class<T> clazz) {\n        final List<T> entries = ServiceLoader.load(clazz).stream().map(ServiceLoader.Provider::get).toList();\n        Constants.LOG.debug(\"Loaded {} entries for {}. {}\", entries.size(), clazz, entries.stream().map(entry -> entry.getClass().getCanonicalName()).collect(Collectors.joining()));\n        return entries;\n    }\n\n    /**\n     * Finds implementations of a service without initializing or classloading them.\n     *\n     * @param name The fully qualified name of the service.\n     * @return A list of all implementations that were found.\n     * @throws IOException Sometimes stuff can't be read.\n     */\n    public static List<String> findServices(String name) throws IOException {\n        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n        final List<String> matches = new ArrayList<>();\n        final Enumeration<URL> candidates = classLoader.getResources(\"META-INF/services/\" + name);\n        while (candidates.hasMoreElements()) {\n            try (InputStream input = candidates.nextElement().openStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    line = line.trim();\n                    if (!line.startsWith(\"#\") && !line.isEmpty()) {\n                        matches.add(line);\n                    }\n                }\n            }\n        }\n        return matches;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/font/BuiltinFonts.java",
    "content": "package net.darkhax.bookshelf.common.api.text.font;\n\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.Set;\n\n/**\n * Constant references to all built-in Minecraft fonts. These fonts are included in the original game and are not\n * bundled or provided by Bookshelf.\n */\npublic enum BuiltinFonts implements IFontEntry {\n\n    /**\n     * The default pixel font that appears in the game.\n     */\n    DEFAULT(\"default\"),\n\n    /**\n     * A magical font based on the Standard Galactic Alphabet. This font is used by the enchanting table and is\n     * associated with the enchantment system.\n     */\n    ALT(\"alt\"),\n\n    /**\n     * A rune font that is used by the Illagers in Minecraft Dungeons.\n     */\n    ILLAGER(\"illageralt\"),\n\n    /**\n     * A plain font that is not stylized.\n     */\n    UNIFORM(\"uniform\");\n\n    public static final Set<ResourceLocation> FONT_IDS = Set.of(DEFAULT.fontId, ALT.fontId, ILLAGER.fontId, UNIFORM.fontId);\n\n    private final ResourceLocation fontId;\n\n    BuiltinFonts(String path) {\n        this(ResourceLocation.tryBuild(\"minecraft\", path));\n    }\n\n    BuiltinFonts(ResourceLocation fontID) {\n        this.fontId = fontID;\n    }\n\n    @Override\n    public ResourceLocation identifier() {\n        return this.fontId;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/font/IFontEntry.java",
    "content": "package net.darkhax.bookshelf.common.api.text.font;\n\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.resources.ResourceLocation;\n\npublic interface IFontEntry {\n\n    /**\n     * Gets the ID of the font.\n     *\n     * @return The font ID.\n     */\n    ResourceLocation identifier();\n\n    /**\n     * Gets the localized name of the font.\n     *\n     * @return The localized name of the font.\n     */\n    default MutableComponent displayName() {\n        return TextHelper.fromResourceLocation(\"font\", null, this.identifier());\n    }\n\n    /**\n     * Gets a description of the font.\n     *\n     * @return A description of the font.\n     */\n    default MutableComponent description() {\n        return TextHelper.fromResourceLocation(\"font\", \"desc\", this.identifier());\n    }\n\n    /**\n     * Gets some text that can be used as a preview for the font in-game.\n     *\n     * @return The preview text for the font.\n     */\n    default MutableComponent preview() {\n        return TextHelper.fromResourceLocation(\"font\", \"preview\", this.identifier());\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/format/IPropertyFormat.java",
    "content": "package net.darkhax.bookshelf.common.api.text.format;\n\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.resources.ResourceLocation;\n\npublic interface IPropertyFormat {\n\n    /**\n     * A namespaced identifier that is used to derive localization keys for the format.\n     *\n     * @return The namespace ID for the format.\n     */\n    ResourceLocation formatKey();\n\n    /**\n     * Formats a property and value using the alignment.\n     *\n     * @param property The name of the property.\n     * @param value    The value of the property.\n     * @return A component that represents an aligned property and value.\n     */\n    default MutableComponent format(Component property, Component value) {\n        return TextHelper.fromResourceLocation(\"format\", null, this.formatKey(), property, value);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/format/PropertyFormat.java",
    "content": "package net.darkhax.bookshelf.common.api.text.format;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * Formats a property string using various separator patterns.\n */\npublic enum PropertyFormat implements IPropertyFormat {\n\n    /**\n     * Formats a property with the separator aligned to the right. Example: \"property: value\".\n     */\n    RIGHT(\"right\"),\n\n    /**\n     * Formats a property with the separator aligned in the center. Example: \"property : value\".\n     */\n    CENTER(\"center\"),\n\n    /**\n     * Formats a property with the separator aligned to the left. Example: \"property :value\".\n     */\n    LEFT(\"left\"),\n\n    /**\n     * Formats a property using a single space as the separator. Example: \"property value\".\n     */\n    SPACED(\"spaced\"),\n\n    /**\n     * Formats a property without a separator. Example: \"propertyvalue\".\n     */\n    NONE(\"none\");\n\n    private final ResourceLocation formatKey;\n\n    PropertyFormat(String key) {\n        this.formatKey = Constants.id(key);\n    }\n\n    @Override\n    public ResourceLocation formatKey() {\n        return this.formatKey;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/unit/IUnit.java",
    "content": "package net.darkhax.bookshelf.common.api.text.unit;\n\nimport net.darkhax.bookshelf.common.api.text.format.PropertyFormat;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.resources.ResourceLocation;\n\npublic interface IUnit {\n\n    /**\n     * A namespaced identifier that is used to derive localization keys for the unit.\n     *\n     * @return The namespace ID for the unit.\n     */\n    ResourceLocation unitKey();\n\n    /**\n     * Gets the name of the unit.\n     *\n     * @return The name of the unit.\n     */\n    default MutableComponent unitName() {\n        return Component.translatable(\"units.\" + this.unitKey().getNamespace() + \".\" + this.unitKey().getPath());\n    }\n\n    /**\n     * Gets the plural name of the unit.\n     *\n     * @return The plural name of the unit.\n     */\n    default MutableComponent plural() {\n        return Component.translatable(\"units.bookshelf.\" + this.unitKey().getNamespace() + \".\" + this.unitKey() + \".plural\");\n    }\n\n    /**\n     * Gets the abbreviated name of the unit.\n     *\n     * @return The abbreviated name of the unit.\n     */\n    default MutableComponent abbreviated() {\n        return Component.translatable(\"units.\" + this.unitKey().getNamespace() + \".\" + this.unitKey() + \".abbreviated\");\n    }\n\n    /**\n     * Formats an amount of the unit as a text component.\n     *\n     * @param amount The amount of the unit.\n     * @return The formatted text. If the amount is plural the plural name will be used.\n     */\n    default MutableComponent format(int amount) {\n        return format(amount, PropertyFormat.LEFT);\n    }\n\n    /**\n     * Formats an amount of the unit as a text component.\n     *\n     * @param amount The amount of the unit.\n     * @param format The property format to use when displaying the unit and value.\n     * @return The formatted text. If the amount is plural the plural name will be used instead.\n     */\n    default MutableComponent format(int amount, PropertyFormat format) {\n        return format.format((amount == 1 || amount == -1) ? this.unitName() : this.plural(), Component.literal(Integer.toString(amount)));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/text/unit/Units.java",
    "content": "package net.darkhax.bookshelf.common.api.text.unit;\n\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * Represents various units that can be displayed in game.\n */\npublic enum Units implements IUnit {\n\n    TICK(\"tick\"),\n    NANOSECOND(\"nanosecond\"),\n    MILLISECOND(\"millisecond\"),\n    SECOND(\"second\"),\n    MINUTE(\"minute\"),\n    HOUR(\"hour\"),\n    DAY(\"day\"),\n    WEEK(\"week\"),\n    MONTH(\"month\"),\n    YEAR(\"year\");\n\n    private final ResourceLocation key;\n\n    Units(String key) {\n        this.key = Constants.id(key);\n    }\n\n    @Override\n    public ResourceLocation unitKey() {\n        return this.key;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/CommandHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport com.mojang.brigadier.arguments.BoolArgumentType;\nimport com.mojang.brigadier.builder.ArgumentBuilder;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport net.darkhax.bookshelf.common.api.commands.IEnumCommand;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.arguments.EntityArgument;\nimport net.minecraft.commands.arguments.selector.EntitySelector;\nimport net.minecraft.world.entity.Entity;\n\nimport java.util.function.Supplier;\n\npublic class CommandHelper {\n\n    /**\n     * Creates a command with branching paths that represent the values of an enum.\n     *\n     * @param parent    The name of the root parent command node.\n     * @param enumClass The enum class to use.\n     * @param <T>       The type of the enum.\n     * @return The newly created command node.\n     */\n    public static <T extends Enum<T> & IEnumCommand> LiteralArgumentBuilder<CommandSourceStack> buildFromEnum(String parent, Class<T> enumClass) {\n        final LiteralArgumentBuilder<CommandSourceStack> parentNode = LiteralArgumentBuilder.literal(parent);\n        parentNode.requires(getLowestLevel(enumClass));\n        buildFromEnum(parentNode, enumClass);\n        return parentNode;\n    }\n\n    /**\n     * Creates branching command paths that represent the values of an enum.\n     *\n     * @param parent    The parent node to branch off from.\n     * @param enumClass The enum class to use.\n     * @param <T>       The type of the enum.\n     */\n    public static <T extends Enum<T> & IEnumCommand> void buildFromEnum(ArgumentBuilder<CommandSourceStack, ?> parent, Class<T> enumClass) {\n        if (!enumClass.isEnum()) {\n            throw new IllegalStateException(\"Class '\" + enumClass.getCanonicalName() + \"' is not an enum!\");\n        }\n        for (T enumEntry : enumClass.getEnumConstants()) {\n            final LiteralArgumentBuilder<CommandSourceStack> literal = LiteralArgumentBuilder.literal(enumEntry.getCommandName());\n            literal.requires(enumEntry.requiredPermissionLevel()).executes(enumEntry);\n            parent.then(literal);\n        }\n    }\n\n    /**\n     * Gets the lowest required permission level for an enum command.\n     *\n     * @param enumClass The enum class to use.\n     * @param <T>       The type of the enum.\n     * @return The lowest required permission level for an enum command.\n     */\n    public static <T extends Enum<T> & IEnumCommand> PermissionLevel getLowestLevel(Class<T> enumClass) {\n        if (!enumClass.isEnum()) {\n            throw new IllegalStateException(\"Class '\" + enumClass.getCanonicalName() + \"' is not an enum!\");\n        }\n        PermissionLevel level = PermissionLevel.OWNER;\n        for (T enumEntry : enumClass.getEnumConstants()) {\n            if (enumEntry.requiredPermissionLevel().get() < level.get()) {\n                level = enumEntry.requiredPermissionLevel();\n            }\n        }\n        return level;\n    }\n\n    /**\n     * @deprecated This only works on Fabric.\n     */\n    @Deprecated\n    public static <T> boolean hasArgument(String argument, CommandContext<T> context) {\n        return hasArgument(argument, context, Object.class);\n    }\n\n    public static <T, C> boolean hasArgument(String argument, CommandContext<C> context, Class<T> argType) {\n        try {\n            return context.getArgument(argument, argType) != null;\n        }\n        catch (Exception e) {\n            return false;\n        }\n    }\n\n    public static <T, C> T getArgument(String argument, CommandContext<C> context, Class<T> argType, Supplier<T> fallback) {\n        return hasArgument(argument, context, argType) ? context.getArgument(argument, argType) : fallback.get();\n    }\n\n    public static Entity getEntity(String argName, CommandContext<CommandSourceStack> ctx, Supplier<Entity> fallback) throws CommandSyntaxException {\n        return CommandHelper.hasArgument(argName, ctx, EntitySelector.class) ? EntityArgument.getEntity(ctx, argName) : fallback.get();\n    }\n\n    public static Entity getEntityOrSender(String argName, CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {\n        return CommandHelper.hasArgument(argName, ctx, EntitySelector.class) ? EntityArgument.getEntity(ctx, argName) : ctx.getSource().getEntity();\n    }\n\n    public static boolean getBooleanArg(String argName, CommandContext<CommandSourceStack> ctx, Supplier<Boolean> fallback) {\n        return CommandHelper.hasArgument(argName, ctx, Boolean.class) ? BoolArgumentType.getBool(ctx, argName) : fallback.get();\n    }\n\n    public static boolean getBooleanArg(String argName, CommandContext<CommandSourceStack> ctx) {\n        return getBooleanArg(argName, ctx, () -> false);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/DataHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport com.mojang.serialization.MapCodec;\nimport io.netty.buffer.ByteBuf;\nimport net.minecraft.core.HolderLookup;\nimport net.minecraft.core.HolderSet;\nimport net.minecraft.core.Registry;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.ListTag;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.crafting.Recipe;\nimport net.minecraft.world.item.crafting.RecipeSerializer;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Optional;\nimport java.util.function.Predicate;\n\npublic class DataHelper {\n\n    public static <T> HolderSet<T> getTagOrEmpty(@Nullable HolderLookup.Provider provider, ResourceKey<Registry<T>> registryKey, TagKey<T> tag) {\n        if (provider != null) {\n            final Optional<HolderSet.Named<T>> optional = provider.lookupOrThrow(registryKey).get(tag);\n            if (optional.isPresent()) {\n                return optional.get();\n            }\n        }\n        return HolderSet.direct();\n    }\n\n    /**\n     * Creates a sublist of a ListTag. Unlike {@link java.util.AbstractList#subList(int, int)}, the sublist is a new\n     * list instance and changes to the original will not be propagated.\n     *\n     * @param list The list to create a sublist from.\n     * @param from The starting index.\n     * @param to   The ending index.\n     * @return A sublist created from the input list.\n     */\n    public static ListTag subList(ListTag list, int from, int to) {\n        if (list == null) {\n            throw new IllegalStateException(\"The input list must not be null!\");\n        }\n        if (from < 0 || to > list.size() || from > to) {\n            throw new IndexOutOfBoundsException(\"Invalid range! from=\" + from + \" to=\" + to + \" size=\" + list.size());\n        }\n        final ListTag subList = new ListTag();\n        for (int i = from; i < to; i++) {\n            subList.add(list.get(i));\n        }\n        return subList;\n    }\n\n    /**\n     * Creates a sublist of an inventory tag based on a predicate on the slot indexes.\n     *\n     * @param list  The inventory list tag.\n     * @param slots A predicate for which item slots should be included in the sublist.\n     * @return A sublist created from the input list.\n     */\n    public static ListTag containerSubList(ListTag list, Predicate<Integer> slots) {\n        if (list == null) {\n            throw new IllegalStateException(\"The input list must not be null!\");\n        }\n        final ListTag subList = new ListTag();\n        for (Tag tag : list) {\n            if (tag instanceof CompoundTag entry && entry.contains(\"Slot\", Tag.TAG_BYTE) && slots.test(entry.getInt(\"Slot\"))) {\n                subList.add(tag);\n            }\n        }\n        return subList;\n    }\n\n    /**\n     * Creates a new stream codec for an optional value.\n     *\n     * @param streamCodec A codec that can serialize the content type.\n     * @param <B>         The type of the byte buffer.\n     * @param <V>         The content type of the stream.\n     * @return An optional stream codec.\n     */\n    public static <B extends ByteBuf, V> StreamCodec<B, Optional<V>> optionalStream(StreamCodec<B, V> streamCodec) {\n        return StreamCodec.of(\n                (buf, val) -> {\n                    buf.writeBoolean(val.isPresent());\n                    val.ifPresent(v -> streamCodec.encode(buf, v));\n                },\n                buf -> {\n                    if (buf.readBoolean()) {\n                        final V val = streamCodec.decode(buf);\n                        return Optional.of(val);\n                    }\n                    return Optional.empty();\n                });\n    }\n\n    /**\n     * Creates a new recipe serializer.\n     *\n     * @param codec  A codec for JSON/NBT data.\n     * @param stream A codec for networking.\n     * @param <T>    The type of the recipe.\n     * @return A recipe serializer object.\n     */\n    public static <T extends Recipe<?>> RecipeSerializer<T> recipeSerializer(MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> stream) {\n        return new RecipeSerializer<>() {\n            @NotNull\n            @Override\n            public MapCodec<T> codec() {\n                return codec;\n            }\n\n            @NotNull\n            @Override\n            public StreamCodec<RegistryFriendlyByteBuf, T> streamCodec() {\n                return stream;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/ExperienceHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.minecraft.world.entity.player.Player;\n\npublic final class ExperienceHelper {\n\n    /**\n     * Attempts to charge the player an experience point cost. If the player can not afford the full amount they will\n     * not be charged and false will be returned.\n     *\n     * @param player The player to charge.\n     * @param cost   The amount to charge the player in experience points.\n     * @return True if the amount was paid.\n     */\n    public static boolean chargeExperiencePoints(Player player, int cost) {\n\n        final int playerExperience = getExperiencePoints(player);\n\n        if (playerExperience >= cost) {\n\n            player.giveExperiencePoints(-cost);\n\n            // The underlying EXP system uses a float which is prone to rounding errors. This will sometimes leave\n            // players with a small fraction of exp progress that is worth less than 1 exp point. These rounding errors\n            // are so small that they do not introduce functionality issues however they can trigger a vanilla bug\n            // where the EXP bar will still render a few pixels of progress even when the player has no exp points. To\n            // prevent this issue here we simply reset all progress when the player spends all of their points.\n            if (getExperiencePoints(player) <= 0) {\n\n                player.experienceProgress = 0f;\n            }\n\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Calculates the amount of experience points the player currently has. This should be used in favour of\n     * {@link Player#totalExperience} which deceptively does not track the amount of experience the player currently\n     * has.\n     * <p>\n     * Contrary to popular belief the {@link Player#totalExperience} value actually loosely represents how much\n     * experience points the player has earned during their current life. This value is akin to the old player score\n     * metric and appears to be predominantly legacy code. Relying on this value is often incorrect as negative changes\n     * to the player level such as enchanting, the anvil, and the level command will not reduce this value.\n     *\n     * @param player The player to calculate the total experience points of.\n     * @return The amount of experience points held by the player.\n     */\n    public static int getExperiencePoints(Player player) {\n\n        // Start by calculating how many EXP points the player's current level is worth.\n        int exp = getTotalPointsForLevel(player.experienceLevel);\n\n        // Add the amount of experience points the player has earned towards their next level.\n        exp += player.experienceProgress * getTotalPointsForLevel(player.experienceLevel + 1);\n\n        return exp;\n    }\n\n    /**\n     * Calculates the amount of additional experience points required to reach the given level when starting from the\n     * previous level. This will also be the amount of experience points that an individual level is worth.\n     *\n     * @param level The level to calculate the point step for.\n     * @return The amount of points required to reach the given level when starting from the previous level.\n     */\n    public static int getPointForLevel(int level) {\n\n        if (level == 0) {\n\n            return 0;\n        }\n\n        else if (level > 30) {\n\n            return 112 + (level - 31) * 9;\n        }\n\n        else if (level > 15) {\n\n            return 37 + (level - 16) * 5;\n        }\n\n        else {\n\n            return 7 + (level - 1) * 2;\n        }\n    }\n\n    /**\n     * Calculates the amount of additional experience points required to reach the target level when starting from the\n     * starting level.\n     *\n     * @param startingLevel The level to start the calculation at.\n     * @param targetLevel   The level to reach.\n     * @return The amount of additional experience points required to go from the starting level to the target level.\n     */\n    public static int getPointsForLevel(int startingLevel, int targetLevel) {\n\n        if (targetLevel < startingLevel) {\n\n            throw new IllegalArgumentException(\"Starting level must be lower than the target level!\");\n        }\n\n        else if (startingLevel < 0) {\n\n            throw new IllegalArgumentException(\"Level bounds must be positive!\");\n        }\n\n        // If the levels are the same there is no point difference.\n        else if (targetLevel == startingLevel) {\n\n            return 0;\n        }\n\n        int requiredPoints = 0;\n\n        for (int lvl = startingLevel + 1; lvl <= targetLevel; lvl++) {\n\n            requiredPoints += getPointForLevel(lvl);\n        }\n\n        return requiredPoints;\n    }\n\n    /**\n     * Calculates the total amount of experience points required to reach a given level when starting at level 0.\n     *\n     * @param level The target level to reach.\n     * @return The amount of experience points required to reach the target level.\n     */\n    public static int getTotalPointsForLevel(int level) {\n\n        return getPointsForLevel(0, level);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/FunctionHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport com.mojang.datafixers.util.Either;\n\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\npublic class FunctionHelper {\n\n    /**\n     * Tests an optional value. If the value is empty or the test fails it will return false.\n     *\n     * @param input The input value to test.\n     * @param test  The test to perform.\n     * @param <T>   The type of value to test.\n     * @return If the test was successful.\n     */\n    public static <T> boolean test(Optional<T> input, Predicate<T> test) {\n        return input.isEmpty() || test.test(input.get());\n    }\n\n    /**\n     * Unpacks an Either into its value using the first possible match.\n     *\n     * @param either The Either to resolve.\n     * @param <T>    The type of value held by the Either.\n     * @return The first value that was unpacked.\n     */\n    public static <T> T unpack(Either<T, T> either) {\n        return either.map(Function.identity(), Function.identity());\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/IGameplayHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.core.NonNullList;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.Container;\nimport net.minecraft.world.WorldlyContainerHolder;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.world.level.block.entity.HopperBlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BiFunction;\n\npublic interface IGameplayHelper {\n\n    RandomSource RNG = RandomSource.create();\n\n    /**\n     * Gets the crafting remainder for a given item. This is required as some platforms have different logic for\n     * determining the crafting remainder.\n     *\n     * @param input The input item.\n     * @return The crafting remainder, or empty if none.\n     */\n    default ItemStack getCraftingRemainder(ItemStack input) {\n        if (input.getItem().hasCraftingRemainingItem()) {\n            final Item remainder = input.getItem().getCraftingRemainingItem();\n            if (remainder != null) {\n                return remainder.getDefaultInstance();\n            }\n        }\n        return ItemStack.EMPTY;\n    }\n\n    /**\n     * If an inventory exists at the specified position, attempt to insert the item into all available slots until the\n     * item has been fully inserted or no more slots are available.\n     *\n     * @param level The world instance.\n     * @param pos   The position of the block.\n     * @param side  The side you are accessing the inventory from. This is from the perspective of the inventory, not\n     *              your block. For example a hopper on top of a chest is inserting downwards but would use the upwards\n     *              face because that is the side of the chest being accessed.\n     * @param stack The item to try inserting.\n     * @return The remaining items that were not inserted.\n     */\n    default ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) {\n        if (stack.isEmpty()) {\n            return stack;\n        }\n        final Container container = getContainer(level, pos);\n        return container != null ? HopperBlockEntity.addItem(null, container, stack, side) : stack;\n    }\n\n    /**\n     * Gets a vanilla container for a given position. This method supports block based containers like the composter,\n     * and block entity based containers like a chest or barrel.\n     *\n     * @param level The world instance.\n     * @param pos   The position to check.\n     * @return The container that was found, or null if no container exists.\n     */\n    @Nullable\n    default Container getContainer(ServerLevel level, BlockPos pos) {\n        final BlockState state = level.getBlockState(pos);\n        if (state.getBlock() instanceof WorldlyContainerHolder holder) {\n            return holder.getContainer(state, level, pos);\n        }\n        final BlockEntity be = level.getBlockEntity(pos);\n        if (be instanceof Container beContainer) {\n            return beContainer;\n        }\n        return null;\n    }\n\n    /**\n     * Attempts to add an item to a list based inventory. This code will try to insert into all available slots until\n     * the item has been completely inserted or no items remain.\n     *\n     * @param stack     The item to add into the inventory.\n     * @param inventory The list of items to add to.\n     * @param slots     An array of valid slots to add to.\n     * @return The remaining items that were not inserted.\n     */\n    default ItemStack addItem(ItemStack stack, NonNullList<ItemStack> inventory, int[] slots) {\n        for (int slot : slots) {\n            if (stack.isEmpty()) {\n                return stack;\n            }\n            final ItemStack existing = inventory.get(slot);\n            if (existing.isEmpty()) {\n                inventory.set(slot, stack);\n                return ItemStack.EMPTY;\n            }\n            else if (existing.getCount() < existing.getMaxStackSize() && ItemStack.isSameItemSameComponents(existing, stack)) {\n                final int availableSpace = existing.getMaxStackSize() - existing.getCount();\n                final int movedAmount = Math.min(stack.getCount(), availableSpace);\n                stack.shrink(movedAmount);\n                existing.grow(movedAmount);\n            }\n        }\n        return stack;\n    }\n\n    /**\n     * Creates a new block entity builder using platform specific code. This is required because the underlying block\n     * entity factory is not accessible.\n     *\n     * @param factory     A factory that creates a new block entity instance.\n     * @param validBlocks The array of valid blocks for the block entity.\n     * @param <T>         The type of the block entity.\n     * @return A new builder for your block entity type.\n     */\n    <T extends BlockEntity> BlockEntityType.Builder<T> blockEntityBuilder(BiFunction<BlockPos, BlockState, T> factory, Block... validBlocks);\n\n    /**\n     * Drops the crafting remainder of an item into the world if the item has one.\n     *\n     * @param level The world to drop the item within.\n     * @param pos   The position to spawn the items at.\n     * @param old   The base item to spawn a remainder from.\n     */\n    default void dropRemainders(Level level, BlockPos pos, ItemStack old) {\n        if (!level.isClientSide && !old.isEmpty()) {\n            final ItemStack remainder = this.getCraftingRemainder(old);\n            if (!remainder.isEmpty()) {\n                Block.popResource(level, pos, remainder.copy());\n            }\n        }\n    }\n\n    CreativeModeTab.Builder tabBuilder();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/IPlatformHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.darkhax.bookshelf.common.api.ModEntry;\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.Set;\n\n/**\n * The PlatformHelper provides useful context and information about the platform the game is running on.\n */\npublic interface IPlatformHelper {\n\n    /**\n     * Gets the working directory path of the game directory.\n     *\n     * @return The working directory path of the game directory.\n     */\n    Path getGamePath();\n\n    /**\n     * Gets the working directory of the game as a File.\n     *\n     * @return The working directory of the game.\n     */\n    default File getGameDirectory() {\n        return this.getGamePath().toFile();\n    }\n\n    /**\n     * Gets the specified configuration path for the game.\n     *\n     * @return The specified configuration path for the game.\n     */\n    Path getConfigPath();\n\n    /**\n     * Gets the specified configuration directory as a file reference.\n     *\n     * @return The specified configuration path for the game.\n     */\n    default File getConfigDirectory() {\n        return this.getConfigPath().toFile();\n    }\n\n    /**\n     * Gets the primary path that the current loader will load mods from.\n     *\n     * @return The currently specified mods path.\n     */\n    Path getModsPath();\n\n    /**\n     * Gets the primary directory that the current loader will load mods from.\n     *\n     * @return The currently specified mods directory.\n     */\n    default File getModsDirectory() {\n        return this.getModsPath().toFile();\n    }\n\n    /**\n     * Checks if a given mod is loaded.\n     *\n     * @param modId The mod id to search for.\n     * @return True when the specified mod id has been loaded.\n     */\n    boolean isModLoaded(String modId);\n\n    /**\n     * Checks if the mod is running in a development environment.\n     *\n     * @return True when the mod is running in a developer environment.\n     */\n    boolean isDevelopmentEnvironment();\n\n    /**\n     * Gets the physical environment that the code is running on.\n     *\n     * @return The physical environment that the code is running on.\n     */\n    PhysicalSide getPhysicalSide();\n\n    /**\n     * Checks if the code is running on a physical client.\n     *\n     * @return Returns true when the code is running on a physical client.\n     */\n    default boolean isPhysicalClient() {\n        return this.getPhysicalSide().isClient();\n    }\n\n    /**\n     * Gets a set of every loaded modId.\n     *\n     * @return A set of all loaded mods.\n     */\n    Set<ModEntry> getLoadedMods();\n\n    /**\n     * Checks if the mod is currently running in an environment with game tests enabled.\n     *\n     * @return Are game tests currently enabled?\n     */\n    boolean isTestingEnvironment();\n\n    /**\n     * Gets the name of the platform.\n     *\n     * @return The name of the platform.\n     */\n    String getName();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/IRenderHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.inventory.InventoryMenu;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.material.FluidState;\nimport org.joml.Matrix4f;\n\npublic interface IRenderHelper {\n\n    IRenderHelper GET = Services.load(IRenderHelper.class);\n    default TextureAtlasSprite blockSprite(ResourceLocation texturePath) {\n        return Minecraft.getInstance().getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(texturePath);\n    }\n\n    void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay);\n\n    default int[] unpackARGB(int color) {\n        return new int[]{color >> 24 & 0xff, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff};\n    }\n\n    default void renderBox(VertexConsumer builder, PoseStack stack, TextureAtlasSprite sprite, int light, int overlay, int[] color) {\n        renderBox(builder, stack.last().pose(), sprite, light, overlay, 0f, 1f, 0f, 1f, 0f, 1f, color);\n    }\n\n    default void renderBox(VertexConsumer builder, PoseStack stack, TextureAtlasSprite sprite, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) {\n        renderBox(builder, stack.last().pose(), sprite, light, overlay, x1, x2, y1, y2, z1, z2, color);\n    }\n\n    default void renderBox(VertexConsumer builder, Matrix4f pos, TextureAtlasSprite sprite, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) {\n        renderFace(builder, pos, sprite, Direction.DOWN, light, overlay, x1, x2, y1, y2, z1, z2, color);\n        renderFace(builder, pos, sprite, Direction.UP, light, overlay, x1, x2, y1, y2, z1, z2, color);\n        renderFace(builder, pos, sprite, Direction.NORTH, light, overlay, x1, x2, y1, y2, z1, z2, color);\n        renderFace(builder, pos, sprite, Direction.SOUTH, light, overlay, x1, x2, y1, y2, z1, z2, color);\n        renderFace(builder, pos, sprite, Direction.WEST, light, overlay, x1, x2, y1, y2, z1, z2, color);\n        renderFace(builder, pos, sprite, Direction.EAST, light, overlay, x1, x2, y1, y2, z1, z2, color);\n    }\n\n    default void renderFace(VertexConsumer builder, Matrix4f pos, TextureAtlasSprite sprite, Direction side, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) {\n        switch (side) {\n            case DOWN -> {\n                final float u1 = sprite.getU(x1);\n                final float u2 = sprite.getU(x2);\n                final float v1 = sprite.getV(z1);\n                final float v2 = sprite.getV(z2);\n                builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f);\n                builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f);\n                builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f);\n                builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f);\n            }\n            case UP -> {\n                final float u1 = sprite.getU(x1);\n                final float u2 = sprite.getU(x2);\n                final float v1 = sprite.getV(z1);\n                final float v2 = sprite.getV(z2);\n                builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f);\n                builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f);\n                builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f);\n                builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f);\n            }\n            case NORTH -> {\n                final float u1 = sprite.getU(x1);\n                final float u2 = sprite.getU(x2);\n                final float v1 = sprite.getV(y1);\n                final float v2 = sprite.getV(y2);\n                builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f);\n                builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f);\n                builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f);\n                builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f);\n            }\n            case SOUTH -> {\n                final float u1 = sprite.getU(x1);\n                final float u2 = sprite.getU(x2);\n                final float v1 = sprite.getV(y1);\n                final float v2 = sprite.getV(y2);\n                builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f);\n                builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f);\n                builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f);\n                builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f);\n            }\n            case WEST -> {\n                final float u1 = sprite.getU(y1);\n                final float u2 = sprite.getU(y2);\n                final float v1 = sprite.getV(z1);\n                final float v2 = sprite.getV(z2);\n                builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f);\n                builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f);\n                builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f);\n                builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f);\n            }\n            case EAST -> {\n                final float u1 = sprite.getU(y1);\n                final float u2 = sprite.getU(y2);\n                final float v1 = sprite.getV(z1);\n                final float v2 = sprite.getV(z2);\n                builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f);\n                builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f);\n                builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f);\n                builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f);\n            }\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/MathsHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.phys.AABB;\nimport net.minecraft.world.phys.Vec3;\nimport net.minecraft.world.phys.shapes.VoxelShape;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.security.SecureRandom;\nimport java.text.DecimalFormat;\nimport java.util.Arrays;\nimport java.util.EnumMap;\nimport java.util.Map;\nimport java.util.Random;\n\npublic class MathsHelper {\n\n    /**\n     * An RNG source that can be used in contexts where a more suitable RNG source is not available.\n     */\n    public static final Random RANDOM = new SecureRandom();\n\n    /**\n     * A RandomSource that can be used in contexts where a more suitable RNG source is not available.\n     */\n    public static final RandomSource RANDOM_SOURCE = RandomSource.create();\n\n    /**\n     * A decimal format that will only preserve two decimal places.\n     */\n    public static final DecimalFormat DECIMAL_2 = new DecimalFormat(\"##.##\");\n\n    /**\n     * Checks if a double is within the given range.\n     *\n     * @param min   The smallest value that is valid.\n     * @param max   The largest value that is valid.\n     * @param value The value to check.\n     * @return If the value is within the defined range.\n     */\n    public static boolean inRange(double min, double max, double value) {\n        return value <= max && value >= min;\n    }\n\n    /**\n     * Calculates the distance between two points.\n     *\n     * @param first  The first position.\n     * @param second The second position.\n     * @return The distance between the first and second position.\n     */\n    public static double distance(Vec3 first, Vec3 second) {\n        final double distanceX = first.x - second.x;\n        final double distanceY = first.y - second.y;\n        final double distanceZ = first.z - second.z;\n        return Math.sqrt(distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ);\n    }\n\n    /**\n     * Rounds a double with a certain amount of precision.\n     *\n     * @param value  The value to round.\n     * @param places The amount of decimal places to preserve.\n     * @return The rounded value.\n     */\n    public static double round(double value, int places) {\n        return value >= 0 && places > 0 ? BigDecimal.valueOf(value).setScale(places, RoundingMode.HALF_UP).doubleValue() : value;\n    }\n\n    /**\n     * Generates a pseudorandom number within a given range of values. The range of values is inclusive of the minimum\n     * and maximum value.\n     *\n     * @param rng The RNG source to generate the number.\n     * @param min The minimum value to generate.\n     * @param max The maximum value to generate.\n     * @return A pseudorandom number within the provided range.\n     */\n    public static int nextInt(Random rng, int min, int max) {\n        return rng.nextInt(max - min + 1) + min;\n    }\n\n    /**\n     * Generates a pseudorandom number within a given range of values. The range of values is inclusive of the minimum\n     * and maximum value.\n     *\n     * @param rng The RNG source to generate the number.\n     * @param min The minimum value to generate.\n     * @param max The maximum value to generate.\n     * @return A pseudorandom number within the provided range.\n     */\n    public static int nextInt(RandomSource rng, int min, int max) {\n        return rng.nextIntBetweenInclusive(min, max);\n    }\n\n    /**\n     * Performs an RNG check that has a percent chance to succeed.\n     *\n     * @param chance The chance that the check will succeed.\n     * @return Returns true when the RNG check is successful.\n     */\n    public static boolean percentChance(double chance) {\n        return Math.random() < chance;\n    }\n\n    /**\n     * Calculates the average of many integers.\n     *\n     * @param values The values to average.\n     * @return The average of the input values.\n     */\n    public static float average(int... values) {\n        return Arrays.stream(values).sum() / (float) values.length;\n    }\n\n    /**\n     * Calculates the percentage out of a total.\n     *\n     * @param value The value that is available.\n     * @param total The largest possible value.\n     * @return The calculated percentage.\n     */\n    public static float percentage(int value, int total) {\n        return (float) value / (float) total;\n    }\n\n    /**\n     * Converts a standard pixel measurement to a world-space measurement. This assumes one block in the world\n     * represents 16 pixels.\n     *\n     * @param pixels The amount of pixels.\n     * @return The size of the pixels in the world-space.\n     */\n    public static double pixelSize(int pixels) {\n        return pixels / 16d;\n    }\n\n    /**\n     * Creates an Axis Aligned Bounding Box from a series of pixel measurements.\n     *\n     * @param minX The start on the X axis.\n     * @param minY The start on the Y axis.\n     * @param minZ The start on the Z axis.\n     * @param maxX The end on the X axis.\n     * @param maxY The end on the Y axis.\n     * @param maxZ The end on the Z axis.\n     * @return An AABB that represents a series of block pixel measurements.\n     */\n    public static AABB boundsForPixels(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {\n        return new AABB(pixelSize(minX), pixelSize(minY), pixelSize(minZ), pixelSize(maxX), pixelSize(maxY), pixelSize(maxZ));\n    }\n\n    /**\n     * Creates horizontally rotated variants of a VoxelShape. The input values are considered to be rotated north.\n     *\n     * @param minX The min X of the shape.\n     * @param minY The min Y of the shape.\n     * @param minZ The min Z of the shape.\n     * @param maxX The max X of the shape.\n     * @param maxY The max Y of the shape.\n     * @param maxZ The max Z of the shape.\n     * @return A map of rotated VoxelShape.\n     */\n    public static Map<Direction, VoxelShape> createHorizontalShapes(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {\n\n        final Map<Direction, VoxelShape> shapes = new EnumMap<>(Direction.class);\n        Direction.Plane.HORIZONTAL.forEach(dir -> shapes.put(dir, rotateShape(dir, minX, minY, minZ, maxX, maxY, maxZ)));\n        return shapes;\n    }\n\n    /**\n     * Creates a VoxelShape that has been rotated to face a given direction. The input sizes are considered to be\n     * rotated north already. The up/down rotations are not supported yet.\n     *\n     * @param facing The direction to rotate the shape.\n     * @param x1     The min x coordinate.\n     * @param y1     The min y coordinate.\n     * @param z1     The min z coordinate.\n     * @param x2     The max x coordinate.\n     * @param y2     The max y coordinate.\n     * @param z2     The max z coordinate.\n     * @return The rotated VoxelShape.\n     */\n    public static VoxelShape rotateShape(Direction facing, double x1, double y1, double z1, double x2, double y2, double z2) {\n        return switch (facing) {\n            case NORTH -> Block.box(x1, y1, z1, x2, y2, z2);\n            case EAST -> Block.box(16 - z2, y1, x1, 16 - z1, y2, x2);\n            case SOUTH -> Block.box(16 - x2, y1, 16 - z2, 16 - x1, y2, 16 - z1);\n            case WEST -> Block.box(z1, y1, 16 - x2, z2, y2, 16 - x1);\n            default -> throw new IllegalArgumentException(\"Can not rotate face in direction \" + facing.name());\n        };\n    }\n    \n    /**\n     * Offsets a position horizontally by a random amount.\n     *\n     * @param startPos The starting position to offset from.\n     * @param rng      The RNG source.\n     * @param range    The maximum amount of blocks to offset the position by. This range applies to both the positive\n     *                 and negative directions.\n     * @return The randomly offset position.\n     */\n    public static BlockPos randomOffsetHorizontal(BlockPos startPos, RandomSource rng, int range) {\n        return randomOffset(startPos, rng, range, 0, range);\n    }\n\n    /**\n     * Offsets a position by a random amount within a limited range.\n     *\n     * @param startPos The starting position to offset from.\n     * @param rng      The RNG source.\n     * @param rangeX   The maximum amount of blocks to offset on the X axis.\n     * @param rangeY   The maximum amount of blocks to offset on the Y axis.\n     * @param rangeZ   The maximum amount of blocks to offset on the Z axis.\n     * @return The randomly offset position.\n     */\n    public static BlockPos randomOffset(BlockPos startPos, RandomSource rng, int rangeX, int rangeY, int rangeZ) {\n        if (rangeX < 0 || rangeY < 0 || rangeZ < 0) {\n            throw new IllegalArgumentException(\"Cannot offset position by '\" + rangeX + \", \" + rangeY + \", \" + rangeZ + \"'. Range must be positive!\");\n        }\n        final int offsetX = rangeX != 0 ? rng.nextIntBetweenInclusive(-rangeX, rangeX) : 0;\n        final int offsetY = rangeY != 0 ? rng.nextIntBetweenInclusive(-rangeY, rangeY) : 0;\n        final int offsetZ = rangeZ != 0 ? rng.nextIntBetweenInclusive(-rangeZ, rangeZ) : 0;\n        return startPos.offset(offsetX, offsetY, offsetZ);\n    }\n\n    /**\n     * Encodes an array of bytes into an array of integers.\n     *\n     * @param bytes The bytes to encode.\n     * @return An array of integers that contain the byte data.\n     */\n    public static int[] encodeBytesToInt(byte[] bytes) {\n        final int byteCount = bytes.length;\n        final int msgSize = (byteCount + 3) / 4;\n        final int encodedLength = 1 + msgSize;\n        final int[] result = new int[encodedLength];\n        result[0] = byteCount; // Store the length in the first int\n        for (int i = 0; i < msgSize; i++) {\n            int value = 0;\n            for (int j = 0; j < 4; j++) {\n                final int byteIndex = i * 4 + j;\n                if (byteIndex < byteCount) {\n                    value |= (bytes[byteIndex] & 0xFF) << (24 - j * 8);\n                }\n            }\n            result[i + 1] = value; // Offset by 1 due to length header\n        }\n        return result;\n    }\n\n    /**\n     * Decodes an array of bytes from an array of integers.\n     *\n     * @param data The data to decode.\n     * @return The decoded bytes.\n     */\n    public static byte[] decodeBytesFromInt(int[] data) {\n        if (data.length == 0) {\n            return new byte[0];\n        }\n        final int byteCount = data[0];\n        final byte[] result = new byte[byteCount];\n        for (int i = 0; i < (data.length - 1); i++) {\n            final int value = data[i + 1];\n            for (int j = 0; j < 4; j++) {\n                final int byteIndex = i * 4 + j;\n                if (byteIndex < byteCount) {\n                    result[byteIndex] = (byte) ((value >> (24 - j * 8)) & 0xFF);\n                }\n            }\n        }\n        return result;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/TextHelper.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\nimport net.darkhax.bookshelf.common.api.annotation.OnlyFor;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.api.text.unit.Units;\nimport net.darkhax.bookshelf.common.mixin.access.client.AccessorFontManager;\nimport net.darkhax.bookshelf.common.mixin.access.client.AccessorMinecraft;\nimport net.darkhax.bookshelf.common.mixin.access.entity.AccessorEntity;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.resources.language.I18n;\nimport net.minecraft.network.chat.ClickEvent;\nimport net.minecraft.network.chat.CommonComponents;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.HoverEvent;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.util.StringUtil;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.Level;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\npublic class TextHelper {\n\n    /**\n     * Creates translated text from a resource location.\n     *\n     * @param prefix   The prefix to add at the start of the key.\n     * @param suffix   The suffix to add at the end of the key.\n     * @param location The resource location to use in the key.\n     * @param args     An optional array of arguments to format into the translated text.\n     * @return A translated component based on the resource location.\n     */\n    public static MutableComponent fromResourceLocation(@Nullable String prefix, @Nullable String suffix, ResourceLocation location, Object... args) {\n        final StringBuilder builder = new StringBuilder();\n        if (prefix != null) {\n            builder.append(prefix).append(\".\");\n        }\n        builder.append(location.getNamespace()).append(\".\").append(location.getPath());\n        if (suffix != null) {\n            builder.append(\".\").append(suffix);\n        }\n        return Component.translatable(builder.toString(), args);\n    }\n\n    /**\n     * Formats a duration of time in ticks into its real world time counterpart. This method should ONLY be used if a\n     * world context is not available or if the duration of time is not affected by custom world tick rates.\n     *\n     * @param ticks The duration of ticks.\n     * @return The formatted time duration.\n     */\n    public static MutableComponent formatDuration(int ticks) {\n\n        return formatDuration(ticks, true, 1f);\n    }\n\n    /**\n     * Formats a duration of time in ticks into its real world time counterpart.\n     *\n     * @param ticks The duration of ticks.\n     * @param level The world level that the duration is taking place. This is used to account for custom tick rates set\n     *              using the game rule.\n     * @return The formatted time duration.\n     */\n    public static MutableComponent formatDuration(int ticks, Level level) {\n        return formatDuration(ticks, true, level);\n    }\n\n    /**\n     * Formats a duration of time in ticks into its real world time counterpart.\n     *\n     * @param ticks        The duration of ticks.\n     * @param includeHover Should the raw tick amount be shown when hovering the text?\n     * @param level        The world level that the duration is taking place. This is used to account for custom tick\n     *                     rates set using the game rule.\n     * @return The formatted time duration.\n     */\n    public static MutableComponent formatDuration(int ticks, boolean includeHover, Level level) {\n        return formatDuration(ticks, includeHover, level.tickRateManager().tickrate());\n    }\n\n    /**\n     * Formats a duration of time in ticks into its real world time counterpart.\n     *\n     * @param ticks            The duration of ticks.\n     * @param showTicksOnHover Should the raw tick amount be shown when hovering the text?\n     * @param tickRate         The tick rate of the current world.\n     * @return The formatted time duration.\n     */\n    public static MutableComponent formatDuration(int ticks, boolean showTicksOnHover, float tickRate) {\n        MutableComponent timeText = Component.literal(StringUtil.formatTickDuration(ticks, tickRate));\n        if (showTicksOnHover) {\n            timeText = Units.TICK.format(ticks);\n        }\n        return timeText;\n    }\n\n    /**\n     * Applies hover text to a text component.\n     *\n     * @param base  The base text component to append.\n     * @param hover The text to display while hovering.\n     * @return A component instance with the hover event applied.\n     */\n    public static MutableComponent withHover(Component base, Component hover) {\n        return withHover(base, new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover));\n    }\n\n    /**\n     * Applies hover text based on an entity to a text component.\n     *\n     * @param base  The base text component to append.\n     * @param hover The Entity to display in the hover text.\n     * @return A component instance with the hover event applied.\n     */\n    public static MutableComponent withHover(Component base, Entity hover) {\n        return withHover(base, hoverEvent(hover));\n    }\n\n    /**\n     * Applies hover text based on an item to a text component.\n     *\n     * @param base  The base text component to append.\n     * @param hover The ItemStack to display in the hover text.\n     * @return A component instance with the hover event applied.\n     */\n    public static MutableComponent withHover(Component base, ItemStack hover) {\n        return withHover(base, new HoverEvent(HoverEvent.Action.SHOW_ITEM, new HoverEvent.ItemStackInfo(hover)));\n    }\n\n    /**\n     * Applies a hover event to a text component.\n     *\n     * @param base  The base text component to append.\n     * @param hover The hover event to apply.\n     * @return A component instance with the hover event applied.\n     */\n    public static MutableComponent withHover(Component base, HoverEvent hover) {\n        return mutable(base).withStyle(style -> style.withHoverEvent(hover));\n    }\n\n    /**\n     * Creates a new hover event for an entity.\n     *\n     * @param entity The entity to create a HoverEvent for.\n     * @return When Mixins are available the entity will create its own HoverEvent, otherwise a fallback based on the\n     * default implementation will be used.\n     */\n    public static HoverEvent hoverEvent(Entity entity) {\n        if (entity instanceof AccessorEntity access) {\n            return access.bookshelf$createHoverEvent();\n        }\n        return new HoverEvent(HoverEvent.Action.SHOW_ENTITY, new HoverEvent.EntityTooltipInfo(entity.getType(), entity.getUUID(), entity.getName()));\n    }\n\n    /**\n     * Provides mutable access to a component.\n     *\n     * @param component The component to access.\n     * @return If the component is already mutable the same component instance will be returned. Otherwise, a mutable\n     * copy of the component will be created.\n     */\n    public static MutableComponent mutable(Component component) {\n        return component instanceof MutableComponent mutable ? mutable : component.copy();\n    }\n\n    /**\n     * Recursively applies a font to text and all of its subcomponents.\n     *\n     * @param text The text to apply the font to.\n     * @param font The ID of the font to apply.\n     * @return The input text with the font applied to its style and the style of its subcomponents.\n     */\n    public static Component applyFont(Component text, ResourceLocation font) {\n        if (text == CommonComponents.EMPTY) {\n            return text;\n        }\n        final MutableComponent modified = mutable(text);\n        modified.withStyle(style -> style.withFont(font));\n        modified.getSiblings().forEach(sib -> applyFont(sib, font));\n        return modified;\n    }\n\n    /**\n     * Attempts to localize several different translation keys and will return the first one that is available on the\n     * client. If no keys are mapped the result will be null.\n     *\n     * @param id   An ID the format within each key using basic string formatting. The first parameter is the namespace\n     *             and the second is the path. For example if a key was \"tooltip.{0}.{1}.info\" the ID \"minecraft:stick\"\n     *             will produce a final key of \"tooltip.minecraft.stick.info\".\n     * @param keys An array of translation keys to attempt localizing.\n     * @return A component for the first translation key that is mapped, or null if none of the keys are mapped.\n     */\n    @Nullable\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static MutableComponent lookupTranslationWithAlias(ResourceLocation id, String... keys) {\n        for (String key : keys) {\n            final MutableComponent lookupResult = lookupTranslation(key.formatted(id.getNamespace(), id.getPath()));\n            if (lookupResult != null) {\n                return lookupResult;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Attempts to localize several different translation keys and will return the first one that is available on the\n     * client. If no keys are mapped the result will be null.\n     *\n     * @param keys   An array of translation keys to attempt localizing.\n     * @param params Arguments that are passed into the translated text.\n     * @return A component for the first translation key that is mapped, or null if none of the keys are mapped.\n     */\n    @Nullable\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static MutableComponent lookupTranslationWithAlias(String[] keys, Object... params) {\n        for (String key : keys) {\n            final MutableComponent lookupResult = lookupTranslation(key, params);\n            if (lookupResult != null) {\n                return lookupResult;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Attempts to localize text. If the translation key is not mapped on the client the component will be null.\n     *\n     * @param key  The translation key to localize.\n     * @param args Arguments that are passed into the translated text.\n     * @return If the key can be translated a component will be returned, otherwise null.\n     */\n    @Nullable\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static MutableComponent lookupTranslation(String key, Object... args) {\n        return lookupTranslation(key, (s, o) -> null, args);\n    }\n\n    /**\n     * Attempts to localize text. If the translation key is not mapped on the client the fallback will be used.\n     *\n     * @param key      The translation key to localize.\n     * @param fallback The fallback text to use when the key is unavailable.\n     * @param args     Arguments that are passed into the translated text.\n     * @return If the key can be translated a component will be returned, otherwise the fallback will be used.\n     */\n    @Nullable\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static MutableComponent lookupTranslation(String key, MutableComponent fallback, Object... args) {\n        return lookupTranslation(key, (s, o) -> fallback, args);\n    }\n\n    /**\n     * Attempts to localize text. If the translation key is not mapped on the client it will try to use the fallback.\n     *\n     * @param key      The translation key to localize.\n     * @param fallback A function that provides fallback text based on the original translation key and arguments. Both\n     *                 the function and the result of this function may be null.\n     * @param args     Arguments that are passed into the translated text.\n     * @return If the key can be translated a component will be returned, otherwise the fallback will be used.\n     */\n    @Nullable\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static MutableComponent lookupTranslation(String key, @Nullable BiFunction<String, Object[], MutableComponent> fallback, Object... args) {\n        if (!Services.PLATFORM.isPhysicalClient()) {\n            throw new IllegalStateException(\"Text can not be translated on the server.\");\n        }\n        return I18n.exists(key) ? Component.translatable(key, args) : fallback != null ? fallback.apply(key, args) : null;\n    }\n\n    /**\n     * Creates a text component that will copy the value to the players clipboard when they click it.\n     *\n     * @param text The text to display and copy to the clipboard.\n     * @return A component that displays text and copies that text to the clipboard when the player clicks on it.\n     */\n    public static MutableComponent copyText(String text) {\n        return setCopyText(Component.literal(text), text);\n    }\n\n    /**\n     * Adds a click event to a text component that will copy text to the players clipboard when they click on it.\n     *\n     * @param component The component to attack the click event to.\n     * @param copy      The text to be copied to the clipboard.\n     * @return A text component that will copy the text when the player clicks on it.\n     */\n    public static MutableComponent setCopyText(MutableComponent component, String copy) {\n        return component.withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, copy)));\n    }\n\n    /**\n     * Joins several components together using a separator.\n     *\n     * @param separator The separator to insert between other components.\n     * @param toJoin    The components to join together.\n     * @return A component containing the joint components.\n     */\n    public static MutableComponent join(Component separator, Component... toJoin) {\n        return join(separator, Arrays.stream(toJoin).iterator());\n    }\n\n    /**\n     * Joins several components together using a separator.\n     *\n     * @param separator The separator to insert between other components.\n     * @param toJoin    The components to join together.\n     * @return A component containing the joint components.\n     */\n    public static MutableComponent join(Component separator, Collection<Component> toJoin) {\n        return join(separator, toJoin.iterator());\n    }\n\n    /**\n     * Joins several components together using a separator. Duplicate entries will be ignored and only the first\n     * occurrence will be joint.\n     *\n     * @param separator The separator to insert between other components.\n     * @param toJoin    The components to join together.\n     * @return A component containing the joint components.\n     */\n    public static MutableComponent joinUnique(Component separator, Collection<Component> toJoin) {\n        final Set<Component> entries = new HashSet<>();\n        for (Component toAdd : toJoin) {\n            addUnique(entries, toAdd);\n        }\n        return join(separator, entries);\n    }\n\n    /**\n     * Adds a component to a list, only if the list does not already contain that component.\n     *\n     * @param components The list to add to.\n     * @param toAdd      The component to add.\n     * @return If the component was added or not.\n     */\n    public static boolean addUnique(Collection<Component> components, Component toAdd) {\n        for (Component existing : components) {\n            if (Objects.equals(existing, toAdd) || existing.getContents().equals(toAdd.getContents())) {\n                return false;\n            }\n        }\n        components.add(toAdd);\n        return true;\n    }\n\n    /**\n     * Joins several components together using a separator.\n     *\n     * @param separator The separator to insert between other components.\n     * @param toJoin    The components to join together.\n     * @return A component containing the joint components.\n     */\n    public static MutableComponent join(Component separator, Iterator<Component> toJoin) {\n        final MutableComponent joined = Component.literal(\"\");\n        while (toJoin.hasNext()) {\n            joined.append(toJoin.next());\n            if (toJoin.hasNext()) {\n                joined.append(separator);\n            }\n        }\n        return joined;\n    }\n\n    /**\n     * Finds a set of possible matches within an iterable group of strings. This can be used to take invalid user input\n     * and attempt to find a plausible match using known good values.\n     * <p>\n     * Possible matches are determined using the Levenshtein distance between the input value and the potential\n     * candidates. The Levenshtein distance represents the number of characters that need to be changed in order for the\n     * strings to match. For example \"abc\" to \"def\" has a difference of three, while \"123\" to \"1234\" has a distance of\n     * 1.\n     *\n     * @param input      The input string.\n     * @param candidates An iterable group of possible candidates.\n     * @return A set of possible matches for the input. This set will include all candidates that have the lowest\n     * possible distance. For example if there were 100 candidates and five had a distance of one all five of the lowest\n     * distance values will be returned.\n     */\n    public static Set<String> getPossibleMatches(String input, Iterable<String> candidates) {\n        return getPossibleMatches(input, candidates, Integer.MAX_VALUE);\n    }\n\n    /**\n     * Finds a set of possible matches within an iterable group of strings. This can be used to take invalid user input\n     * and attempt to find a plausible match using known good values.\n     * <p>\n     * Possible matches are determined using the Levenshtein distance between the input value and the potential\n     * candidates. The Levenshtein distance represents the number of characters that need to be changed in order for the\n     * strings to match. For example \"abc\" to \"def\" has a difference of three, while \"123\" to \"1234\" has a distance of\n     * 1.\n     *\n     * @param input      The input string.\n     * @param candidates An iterable group of possible candidates.\n     * @param threshold  The maximum distance allowed for a value to be considered. For example if the threshold is two,\n     *                   only entries with a distance of two or less will be considered.\n     * @return A set of possible matches for the input. This set will include all candidates that have the lowest\n     * possible distance. For example if there were 100 candidates and five had a distance of one all five of the lowest\n     * distance values will be returned.\n     */\n    public static Set<String> getPossibleMatches(String input, Iterable<String> candidates, int threshold) {\n        final HashSet<String> bestMatches = new HashSet();\n        int distance = threshold;\n        for (String candidate : candidates) {\n            final int currentDistance = StringUtils.getLevenshteinDistance(input, candidate);\n            if (currentDistance < distance) {\n                distance = currentDistance;\n                bestMatches.clear();\n                bestMatches.add(candidate);\n            }\n            else if (currentDistance == distance) {\n                bestMatches.add(candidate);\n            }\n        }\n        return bestMatches;\n    }\n\n    /**\n     * Formats a collection of values to a string using {@link Object#toString()}. If the collection has more than one\n     * value each entry will be separated by commas. Each value will also be quoted.\n     *\n     * @param collection The collection of values to format.\n     * @param <T>        The type of value being formatted.\n     * @return The formatted string.\n     */\n    public static <T> String formatCollection(Collection<T> collection) {\n        return formatCollection(collection, entry -> \"\\\"\" + entry.toString() + \"\\\"\", \", \");\n    }\n\n    /**\n     * Formats a collection of values to a string. If the collection has more than one value each entry will be\n     * separated using the delimiter.\n     *\n     * @param collection The collection of values to format.\n     * @param formatter  A function used to format the value to a string.\n     * @param delimiter  A delimiter used to separate values in a list.\n     * @param <T>        The type of value being formatted.\n     * @return The formatted string.\n     */\n    public static <T> String formatCollection(Collection<T> collection, Function<T, String> formatter, String delimiter) {\n        return collection.size() == 1 ? formatter.apply(collection.stream().findFirst().get()) : collection.stream().map(formatter).collect(Collectors.joining(delimiter));\n    }\n\n    @OnlyFor(PhysicalSide.CLIENT)\n    public static Set<ResourceLocation> getRegisteredFonts() {\n        if (!Services.PLATFORM.isPhysicalClient()) {\n            return Collections.emptySet();\n        }\n        return ((AccessorFontManager) (((AccessorMinecraft) Minecraft.getInstance()).bookshelf$getFontManager())).bookshelf$getFonts().keySet();\n    }\n\n    /**\n     * Creates a translation key that should map to a display name for the tag.\n     * <p>\n     * Tags for vanilla registries use the format tag.reg_path.namespace.path and tags for modded registries use the\n     * format tag.reg_namespace.reg_path.namespace.path.\n     * <p>\n     * This is a new standard being pushed by the Fabric API and recipe viewers. While it has not been universally\n     * adopted yet, it should be considered best practice to do so moving forward.\n     *\n     * @param tag The tag to provide a name key for.\n     * @return A translation key that should map to a display name.\n     */\n    public static String getTagName(TagKey<?> tag) {\n        final StringBuilder builder = new StringBuilder();\n        builder.append(\"tag.\");\n        final ResourceLocation regId = tag.registry().location();\n        final ResourceLocation tagId = tag.location();\n        if (!regId.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {\n            builder.append(regId.getNamespace()).append(\".\");\n        }\n        builder.append(regId.getPath().replace(\"/\", \".\")).append(\".\").append(tagId.getNamespace()).append(\".\").append(tagId.getPath().replace(\"/\", \".\").replace(\":\", \".\"));\n        return builder.toString();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/api/util/TickAccumulator.java",
    "content": "package net.darkhax.bookshelf.common.api.util;\n\nimport net.minecraft.world.level.Level;\n\n/**\n * While the current tick rate is synced between the client and server, some things like tile entities continue to tick\n * at the base 20tps on the client. This tick accumulator allows ticks to be accumulates as normal the server, but will\n * scale client ticks to roughly the correct amount.\n */\npublic class TickAccumulator {\n\n    private final float defaultValue;\n    private float ticks;\n\n    /**\n     * Creates a new tick accumulator.\n     *\n     * @param defaultValue The amount to reset to when {@link TickAccumulator#reset()} is used.\n     */\n    public TickAccumulator(float defaultValue) {\n        this.ticks = defaultValue;\n        this.defaultValue = defaultValue;\n    }\n\n    /**\n     * Ticks the accumulator up by one tick.\n     *\n     * @param level The current game level.\n     */\n    public void tickUp(Level level) {\n        this.tick(level.isClientSide ? level.tickRateManager().tickrate() / 20f : 1f);\n    }\n\n    /**\n     * Ticks the accumulator down by one tick.\n     *\n     * @param level The current game level.\n     */\n    public void tickDown(Level level) {\n        this.tick(-(level.isClientSide ? level.tickRateManager().tickrate() / 20f : 1f));\n    }\n\n    /**\n     * Adds an amount of ticks to the accumulator.\n     *\n     * @param amount The amount of ticks to add.\n     */\n    public void tick(float amount) {\n        this.ticks += amount;\n    }\n\n    /**\n     * Get the current amount of ticks.\n     *\n     * @return The current amount of ticks.\n     */\n    public float getTicks() {\n        return this.ticks;\n    }\n\n    /**\n     * Sets the current amount of ticks.\n     *\n     * @param ticks The new tick count.\n     */\n    public void setTicks(float ticks) {\n        this.ticks = ticks;\n    }\n\n    /**\n     * Resets the tick accumulator.\n     */\n    public void reset() {\n        this.ticks = this.defaultValue;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/BookshelfContent.java",
    "content": "package net.darkhax.bookshelf.common.impl;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.darkhax.bookshelf.common.api.commands.args.FontArgument;\nimport net.darkhax.bookshelf.common.api.commands.args.TagArgument;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriptions;\nimport net.darkhax.bookshelf.common.api.registry.ContentProvider;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.command.BlockTagToItemTagCommand;\nimport net.darkhax.bookshelf.common.impl.command.DebugCommands;\nimport net.darkhax.bookshelf.common.impl.command.EnchantCommand;\nimport net.darkhax.bookshelf.common.impl.command.FontCommand;\nimport net.darkhax.bookshelf.common.impl.command.HandCommand;\nimport net.darkhax.bookshelf.common.impl.command.RenameCommand;\nimport net.darkhax.bookshelf.common.impl.command.StructureCommand;\nimport net.darkhax.bookshelf.common.impl.command.TranslateCommand;\nimport net.darkhax.bookshelf.common.impl.data.conditions.And;\nimport net.darkhax.bookshelf.common.impl.data.conditions.ModLoaded;\nimport net.darkhax.bookshelf.common.impl.data.conditions.Not;\nimport net.darkhax.bookshelf.common.impl.data.conditions.OnPlatform;\nimport net.darkhax.bookshelf.common.impl.data.conditions.Or;\nimport net.darkhax.bookshelf.common.impl.data.conditions.RegistryContains;\nimport net.darkhax.bookshelf.common.impl.data.criterion.item.NamespaceItemPredicate;\nimport net.darkhax.bookshelf.common.impl.data.criterion.trigger.AdvancementTrigger;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.AllOfIngredient;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.BlockTagIngredient;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.EitherIngredient;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.FalseIngredient;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.ModIdIngredient;\nimport net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter;\nimport net.minecraft.advancements.CriterionTrigger;\nimport net.minecraft.advancements.critereon.ItemSubPredicate;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntries;\n\npublic class BookshelfContent implements ContentProvider {\n\n    @Override\n    public void defineIngredientTypes(IngredientTypeAdapter registry) {\n        registry.add(\"false\", FalseIngredient.CODEC, FalseIngredient.STREAM);\n        registry.add(\"all\", AllOfIngredient.CODEC, AllOfIngredient.STREAM);\n        registry.add(\"either\", EitherIngredient.CODEC, EitherIngredient.STREAM);\n        registry.add(\"mod_id\", ModIdIngredient.CODEC, ModIdIngredient.STREAM);\n        registry.add(\"block_tag\", BlockTagIngredient.CODEC, BlockTagIngredient.STREAM);\n    }\n\n    @Override\n    public void defineCommands(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext context, Commands.CommandSelection selection) {\n        final LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal(Constants.MOD_ID).requires(PermissionLevel.MODERATOR);\n        root.then(HandCommand.build(context));\n        root.then(FontCommand.build());\n        root.then(RenameCommand.build(context));\n        root.then(EnchantCommand.build(context));\n        root.then(TranslateCommand.build(context));\n        root.then(BlockTagToItemTagCommand.build(context));\n        root.then(StructureCommand.build());\n        if (Services.PLATFORM.isDevelopmentEnvironment() && Services.PLATFORM.isPhysicalClient() && selection == Commands.CommandSelection.INTEGRATED) {\n            root.then(DebugCommands.build(context));\n        }\n        dispatcher.register(root);\n    }\n\n    @Override\n    public void defineCommandArguments(CommandArgumentAdapter registry) {\n        registry.add(\"font\", FontArgument.class, FontArgument.SERIALIZER);\n        registry.add(\"tag\", TagArgument.class, TagArgument.SERIALIZER);\n    }\n\n    @Override\n    public void defineLoadConditions(GenericRegistryAdapter<MapCodec<? extends ILoadCondition>> registry) {\n        registry.add(And.TYPE_ID, And.CODEC);\n        registry.add(Not.TYPE_ID, Not.CODEC);\n        registry.add(Or.TYPE_ID, Or.CODEC);\n        registry.add(OnPlatform.TYPE_ID, OnPlatform.CODEC);\n        registry.add(ModLoaded.TYPE_ID, ModLoaded.CODEC);\n        registry.add(RegistryContains.BLOCK, RegistryContains.of(RegistryContains.BLOCK, BuiltInRegistries.BLOCK));\n        registry.add(RegistryContains.ITEM, RegistryContains.of(RegistryContains.ITEM, BuiltInRegistries.ITEM));\n        registry.add(RegistryContains.ENTITY, RegistryContains.of(RegistryContains.ENTITY, BuiltInRegistries.ENTITY_TYPE));\n        registry.add(RegistryContains.BLOCK_ENTITY, RegistryContains.of(RegistryContains.BLOCK_ENTITY, BuiltInRegistries.BLOCK_ENTITY_TYPE));\n    }\n\n    @Override\n    public void defineItemSubPredicates(GameRegistryAdapter<ItemSubPredicate.Type<?>> registry) {\n        registry.add(\"namespace\", new ItemSubPredicate.Type<>(NamespaceItemPredicate.CODEC));\n    }\n\n    @Override\n    public void defineCriteriaTriggers(GameRegistryAdapter<CriterionTrigger<?>> registry) {\n        registry.add(\"earn_advancement\", AdvancementTrigger.TRIGGER);\n    }\n\n    @Override\n    public void defineLootEntryTypes(LootEntryTypeAdapter registry) {\n        registry.add(\"item_stack\", LootItemStack.CODEC);\n    }\n\n    @Override\n    public void defineLootDescriptions(LootDescriptionAdapter registry) {\n        registry.registryFunc().accept(LootPoolEntries.EMPTY, LootPoolEntryDescriptions.EMPTY);\n        registry.registryFunc().accept(LootPoolEntries.ITEM, LootPoolEntryDescriptions.ITEM);\n        registry.registryFunc().accept(LootPoolEntries.LOOT_TABLE, LootPoolEntryDescriptions.LOOT_TABLE);\n        registry.registryFunc().accept(LootPoolEntries.DYNAMIC, LootPoolEntryDescriptions.DYNAMIC);\n        registry.registryFunc().accept(LootPoolEntries.TAG, LootPoolEntryDescriptions.TAG);\n        registry.registryFunc().accept(LootPoolEntries.ALTERNATIVES, LootPoolEntryDescriptions.COMPOSITE);\n        registry.registryFunc().accept(LootPoolEntries.SEQUENCE, LootPoolEntryDescriptions.COMPOSITE);\n        registry.registryFunc().accept(LootPoolEntries.GROUP, LootPoolEntryDescriptions.COMPOSITE);\n        registry.registryFunc().accept(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE.get(Constants.id(\"item_stack\")), LootPoolEntryDescriptions.ITEM_STACK);\n    }\n\n    @Override\n    public String namespace() {\n        return Constants.MOD_ID;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/BookshelfMod.java",
    "content": "package net.darkhax.bookshelf.common.impl;\n\nimport net.darkhax.bookshelf.common.api.service.Services;\n\nimport java.io.IOException;\nimport java.util.List;\n\npublic class BookshelfMod {\n\n    private static BookshelfMod instance;\n    private boolean hasInitialized = false;\n\n    public void init() {\n        if (hasInitialized) {\n            throw new IllegalStateException(\"The \" + Constants.MOD_NAME + \" has already been initialized.\");\n        }\n\n        this.runStartupChecks();\n\n        hasInitialized = true;\n    }\n\n    private void runStartupChecks() {\n        if (Services.PLATFORM == null) {\n            throw new IllegalStateException(\"Bookshelf services are not available.\");\n        }\n        this.detectInvalidContentProviders();\n    }\n\n    @Deprecated\n    private void detectInvalidContentProviders() {\n        try {\n            final List<String> oldProviders = Services.findServices(\"net.darkhax.bookshelf.common.api.registry.IContentProvider\");\n            if (!oldProviders.isEmpty()) {\n                final String errorMsg = \"An outdated implementation of IContentProvider has been found. The game is being stopped for your protection. Please check if an update is available! More information at https://gist.github.com/Darkhax/63356eed0a27848efe8574ce4c677bae\";\n                Constants.LOG.error(errorMsg);\n                for (String provider : oldProviders) {\n                    Constants.LOG.error(\"- {}\", provider);\n                }\n                throw new IllegalStateException(errorMsg + \" \" + String.join(\", \", oldProviders));\n            }\n        }\n        catch (IOException e) {\n            Constants.LOG.error(\"Failed to read services.\", e);\n        }\n    }\n\n    /**\n     * Gets the Bookshelf mod instance. If an instance does not already exist one will be created.\n     *\n     * @return The Bookshelf mod instance.\n     */\n    public static BookshelfMod getInstance() {\n        if (instance == null) {\n            instance = new BookshelfMod();\n        }\n        return instance;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/Constants.java",
    "content": "package net.darkhax.bookshelf.common.impl;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.lang.ref.WeakReference;\n\npublic class Constants {\n\n    public static final String MOD_ID = \"bookshelf\";\n    public static final String MOD_NAME = \"Bookshelf\";\n    public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME);\n    public static final Gson GSON_PRETTY = new GsonBuilder().setPrettyPrinting().create();\n\n    public static ResourceLocation id(String path) {\n        return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);\n    }\n\n    public static WeakReference<RecipeManager> SERVER_RECIPE_MANAGER;\n    public static int SERVER_REVISION = 0;\n\n    public static WeakReference<RecipeManager> CLIENT_RECIPE_MANAGER;\n    public static int CLIENT_REVISION = 0;\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/DebugContentProvider.java",
    "content": "package net.darkhax.bookshelf.common.impl;\n\nimport net.darkhax.bookshelf.common.api.entity.villager.MerchantTier;\nimport net.darkhax.bookshelf.common.api.entity.villager.trades.VillagerBuys;\nimport net.darkhax.bookshelf.common.api.registry.ContentProvider;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.data.ingredient.AllOfIngredient;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.entity.npc.VillagerProfession;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.item.trading.ItemCost;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.Blocks;\nimport net.minecraft.world.level.block.state.BlockBehaviour;\n\npublic class DebugContentProvider implements ContentProvider {\n\n    @Override\n    public void defineBlocks(BlockRegistryAdapter registry) {\n        registry.add(\"test_block\", () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE)));\n        registry.addPlaceable(\"test_placeable\", () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK)));\n    }\n\n    @Override\n    public void defineItems(GameRegistryAdapter<Item> registry) {\n        registry.add(\"test_item\", () -> new Item(new Item.Properties()));\n    }\n\n    @Override\n    public void defineCreativeTabs(CreativeModeTabAdapter registry) {\n        registry.add(\"test_tab\", Items.BOOKSHELF::getDefaultInstance, (params, output) -> {\n            output.accept(Items.BOOKSHELF);\n            BuiltInRegistries.ITEM.keySet().stream().filter(id -> id.getNamespace().equalsIgnoreCase(Constants.MOD_ID)).forEach(id -> output.accept(BuiltInRegistries.ITEM.get(id)));\n        });\n    }\n\n    @Override\n    public void defineTrades(VillagerTradeAdapter registry) {\n        registry.addTrade(VillagerProfession.ARMORER, MerchantTier.NOVICE, new VillagerBuys(() -> new ItemCost(Items.BEDROCK, 1), 1, 1, 0, 0));\n        registry.addCommonWanderingTrade(new VillagerBuys(() -> new ItemCost(Items.BARRIER, 1), 1, 1, 0, 0));\n        registry.addRareWanderingTrade(new VillagerBuys(() -> new ItemCost(Items.STRUCTURE_VOID, 1), 1, 1, 0, 0));\n    }\n\n    @Override\n    public void defineIngredientTypes(IngredientTypeAdapter registry) {\n        registry.add(\"test_all\", AllOfIngredient.CODEC, AllOfIngredient.STREAM);\n    }\n\n    @Override\n    public String namespace() {\n        return Constants.MOD_ID;\n    }\n\n    @Override\n    public boolean canLoad() {\n        final boolean canLoad = Services.PLATFORM.isDevelopmentEnvironment();\n        if (canLoad) {\n            Constants.LOG.warn(\"Developer mode is enabled! Bookshelf will load its debug content!\");\n            Constants.LOG.warn(\"Bookshelf's debug content will affect the gameplay experience!\");\n            Constants.LOG.warn(\"If you are not in a development environment you really should disable developer mode!\");\n            Constants.LOG.warn(\"If you are a developer you can ignore this message.\");\n        }\n        return canLoad;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/BlockTagToItemTagCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.darkhax.bookshelf.common.api.commands.args.TagArgument;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.core.HolderSet;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.BlockItem;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.Function;\n\npublic class BlockTagToItemTagCommand {\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        final LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal(\"block_to_item_tag\").requires(PermissionLevel.GAMEMASTER);\n        root.then(Commands.argument(\"block_tag\", TagArgument.arg(context, Registries.BLOCK)).executes(ctx -> {\n            final TagKey<Block> result = TagArgument.get(\"block_tag\", ctx, Registries.BLOCK);\n            final HolderSet.Named<Block> tag = BuiltInRegistries.BLOCK.getTag(result).orElseThrow();\n            final Set<BlockItem> items = new HashSet<>();\n            for (Item item : BuiltInRegistries.ITEM) {\n                if (item instanceof BlockItem blockItem && tag.contains(blockItem.getBlock().builtInRegistryHolder())) {\n                    items.add(blockItem);\n                }\n            }\n            ctx.getSource().sendSuccess(() -> TextHelper.copyText(Constants.GSON_PRETTY.toJson(tagJson(items, BuiltInRegistries.ITEM::getKey))), false);\n            return 0;\n        }));\n        return root;\n    }\n\n    private static <T> JsonObject tagJson(Collection<T> entries, Function<T, ResourceLocation> idFunc) {\n        final JsonArray array = new JsonArray();\n        entries.stream().map(entry -> idFunc.apply(entry).toString()).sorted().forEach(array::add);\n        final JsonObject obj = new JsonObject();\n        obj.add(\"values\", array);\n        return obj;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/DebugCommands.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport net.darkhax.bookshelf.common.api.commands.IEnumCommand;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriptions;\nimport net.darkhax.bookshelf.common.api.util.CommandHelper;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.common.impl.data.loot.modifiers.ILootPoolHooks;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable;\nimport net.minecraft.client.resources.language.I18n;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.HolderGetter;\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.storage.loot.LootTable;\n\nimport java.util.Collection;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.StringJoiner;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic enum DebugCommands implements IEnumCommand {\n\n    MISSING_TAG_NAMES(DebugCommands::findMissingTagNames),\n    MISSING_BLOCK_DROPS(DebugCommands::findMissingBlockDrops),\n    LOOT_POOL_HASH(DebugCommands::findLootTableHashes),\n    SIMPLE_TABLES(DebugCommands::printTables);\n\n    private static void printTables(MinecraftServer server, StringJoiner out) {\n        final Collection<ResourceLocation> tableKeys = server.reloadableRegistries().getKeys(Registries.LOOT_TABLE);\n        final RegistryAccess registries = server.reloadableRegistries().get();\n        for (ResourceLocation tableKey : tableKeys) {\n            final LootTable table = server.reloadableRegistries().getLootTable(ResourceKey.create(Registries.LOOT_TABLE, tableKey));\n            out.add(tableKey + \" = \" + LootPoolEntryDescriptions.getUniqueItems(registries, table));\n        }\n    }\n\n    private static void findMissingTagNames(MinecraftServer server, StringJoiner out) {\n        server.registryAccess().registries().forEach(entry -> {\n            AtomicBoolean hasLogged = new AtomicBoolean(false);\n            entry.value().getTagNames().forEach(tag -> {\n                if (!tag.location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE) && !I18n.exists(TextHelper.getTagName(tag))) {\n                    if (!hasLogged.get()) {\n                        hasLogged.set(true);\n                        out.add(\"## \" + entry.key().location());\n                    }\n                    out.add(\"\\\"\" + TextHelper.getTagName(tag) + \"\\\": \\\"\\\",\");\n                }\n            });\n            if (hasLogged.get()) {\n                out.add(\"\");\n            }\n        });\n    }\n\n    private static void findMissingBlockDrops(MinecraftServer server, StringJoiner out) {\n        final HolderGetter<LootTable> lootTables = server.reloadableRegistries().lookup().lookup(Registries.LOOT_TABLE).orElseThrow();\n        for (Block block : BuiltInRegistries.BLOCK) {\n            final Optional<Holder.Reference<LootTable>> result = lootTables.get(block.getLootTable());\n            if (result.isEmpty()) {\n                final ResourceLocation id = block.getLootTable().location();\n                out.add(BuiltInRegistries.BLOCK.getKey(block) + \" - \" + id + \" - data/\" + id.getNamespace() + \"/loot_table/\" + id.getPath());\n            }\n        }\n    }\n\n    private static void findLootTableHashes(MinecraftServer server, StringJoiner out) {\n        final HolderGetter<LootTable> lootTables = server.reloadableRegistries().lookup().lookup(Registries.LOOT_TABLE).orElseThrow();\n        for (ResourceLocation table : server.reloadableRegistries().getKeys(Registries.LOOT_TABLE)) {\n            if (table.getPath().startsWith(\"chests\") || table.getPath().startsWith(\"dispensers\") || table.getPath().startsWith(\"gameplay\") || table.getPath().startsWith(\"pots\") || table.getPath().startsWith(\"spawners\")) {\n                out.add(\"## \" + table);\n                lootTables.get(ResourceKey.create(Registries.LOOT_TABLE, table)).ifPresent(val -> {\n                    if (val.value() instanceof AccessorLootTable accessor) {\n                        for (int index = 0; index < accessor.bookshelf$pools().size(); index++) {\n                            if (accessor.bookshelf$pools().get(index) instanceof ILootPoolHooks fingerprintable) {\n                                out.add(\"- \" + index + \" | \" + fingerprintable.bookshelf$getHash());\n                            }\n                        }\n                    }\n                });\n            }\n        }\n    }\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        return CommandHelper.buildFromEnum(\"debug\", DebugCommands.class);\n    }\n\n    private final DebugTask debugTask;\n\n    DebugCommands(DebugTask task) {\n        this.debugTask = task;\n    }\n\n    @Override\n    public String getCommandName() {\n        return this.name().toLowerCase(Locale.ROOT);\n    }\n\n    @Override\n    public int run(CommandContext<CommandSourceStack> context) {\n        final StringJoiner joiner = new StringJoiner(System.lineSeparator());\n        this.debugTask.getDebugOutput(context.getSource().getServer(), joiner);\n        Constants.LOG.warn(joiner.toString());\n        final String debugInfo = joiner.toString();\n        if (debugInfo.isBlank()) {\n            context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.debug.no_info\"));\n            return 0;\n        }\n        else if (debugInfo.length() > 10000) {\n            context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.debug.too_long\"));\n            return 0;\n        }\n        else {\n            context.getSource().sendSuccess(() -> TextHelper.setCopyText(Component.translatable(\"commands.bookshelf.debug.yes_info\"), debugInfo), false);\n            return 1;\n        }\n    }\n\n    @Override\n    public PermissionLevel requiredPermissionLevel() {\n        return PermissionLevel.OWNER;\n    }\n\n    @FunctionalInterface\n    public interface DebugTask {\n        void getDebugOutput(MinecraftServer server, StringJoiner output);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/EnchantCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.arguments.IntegerArgumentType;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.darkhax.bookshelf.common.api.util.CommandHelper;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.commands.arguments.EntityArgument;\nimport net.minecraft.commands.arguments.ResourceArgument;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.LivingEntity;\nimport net.minecraft.world.item.enchantment.Enchantment;\n\npublic class EnchantCommand {\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        final LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal(\"enchant\").requires(PermissionLevel.GAMEMASTER);\n        root.then(Commands.argument(\"enchantment\", ResourceArgument.resource(context, Registries.ENCHANTMENT)).then(Commands.argument(\"level\", IntegerArgumentType.integer(0)).executes(EnchantCommand::enchantItem)));\n        root.then(Commands.argument(\"target\", EntityArgument.entity()).then(Commands.argument(\"enchantment\", ResourceArgument.resource(context, Registries.ENCHANTMENT)).then(Commands.argument(\"level\", IntegerArgumentType.integer(0)).executes(EnchantCommand::enchantItem))));\n        return root;\n    }\n\n    private static int enchantItem(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {\n        final Entity target = CommandHelper.getEntityOrSender(\"target\", ctx);\n        final Holder.Reference<Enchantment> enchantment = ResourceArgument.getEnchantment(ctx, \"enchantment\");\n        final int level = IntegerArgumentType.getInteger(ctx, \"level\");\n        if (target instanceof LivingEntity livingTarget) {\n            if (livingTarget.getMainHandItem().isEmpty()) {\n                ctx.getSource().sendFailure(Component.translatable(\"commands.bookshelf.hand.error.not_air\"));\n                return 0;\n            }\n            livingTarget.getMainHandItem().enchant(enchantment, level);\n            return Command.SINGLE_SUCCESS;\n        }\n        return 0;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/FontCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.darkhax.bookshelf.common.api.commands.args.FontArgument;\nimport net.darkhax.bookshelf.common.api.util.CommandHelper;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.darkhax.bookshelf.common.mixin.access.block.AccessorBannerBlockEntity;\nimport net.darkhax.bookshelf.common.mixin.access.block.AccessorBaseContainerBlockEntity;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.commands.arguments.EntityArgument;\nimport net.minecraft.commands.arguments.MessageArgument;\nimport net.minecraft.commands.arguments.coordinates.BlockPosArgument;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.LivingEntity;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.block.entity.BannerBlockEntity;\nimport net.minecraft.world.level.block.entity.BaseContainerBlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.SignBlockEntity;\nimport net.minecraft.world.level.block.entity.SignText;\n\nimport java.util.function.UnaryOperator;\n\npublic class FontCommand {\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build() {\n        final LiteralArgumentBuilder<CommandSourceStack> font = Commands.literal(\"font\").requires(PermissionLevel.GAMEMASTER);\n\n        LiteralArgumentBuilder<CommandSourceStack> rename = Commands.literal(\"rename\");\n        rename.then(FontArgument.argument().executes(FontCommand::renameItemWithFont));\n        rename.then(Commands.argument(\"target\", EntityArgument.entity()).then(FontArgument.argument().executes(FontCommand::renameItemWithFont)));\n        font.then(rename);\n\n        font.then(Commands.literal(\"block\").then(FontArgument.argument().then(Commands.argument(\"pos\", BlockPosArgument.blockPos()).executes(FontCommand::renameBlockWithFont))));\n        font.then(Commands.literal(\"say\").then(FontArgument.argument().then(Commands.argument(\"message\", MessageArgument.message()).executes(FontCommand::speakWithFont))));\n        return font;\n    }\n\n    private static int speakWithFont(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        final ResourceLocation fontId = FontArgument.get(context);\n        final Component inputMessage = TextHelper.applyFont(MessageArgument.getMessage(context, \"message\"), fontId);\n        final Component txtMessage = Component.translatable(\"chat.type.announcement\", context.getSource().getDisplayName(), inputMessage);\n        context.getSource().getServer().getPlayerList().broadcastSystemMessage(txtMessage, false);\n        return 0;\n    }\n\n    private static int renameItemWithFont(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n\n        final ResourceLocation fontId = FontArgument.get(context);\n        final Entity target = CommandHelper.getEntityOrSender(\"target\", context);\n\n        if (target instanceof LivingEntity living) {\n            final ItemStack stack = living.getMainHandItem();\n            if (stack.isEmpty()) {\n                context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.hand.error.not_air\"));\n                return 0;\n            }\n            stack.set(DataComponents.CUSTOM_NAME, TextHelper.applyFont(stack.getHoverName().copy(), fontId));\n            return 1;\n        }\n        context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.font.bad_sender\"));\n        return 0;\n    }\n\n    private static int renameBlockWithFont(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        final ServerLevel world = context.getSource().getLevel();\n        final ResourceLocation fontId = FontArgument.get(context);\n        final BlockPos pos = BlockPosArgument.getLoadedBlockPos(context, \"pos\");\n        final BlockEntity tile = world.getBlockEntity(pos);\n        if (tile != null && tile.hasLevel()) {\n            switch (tile) {\n                case BaseContainerBlockEntity container when tile instanceof AccessorBaseContainerBlockEntity accessor ->\n                        accessor.bookshelf$name(TextHelper.applyFont(container.getName().copy(), fontId));\n                case SignBlockEntity sign -> {\n                    if (sign.getLevel() != null) {\n                        sign.updateText(applySignFont(fontId), true);\n                        sign.updateText(applySignFont(fontId), false);\n                        sign.getLevel().sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3);\n                    }\n                }\n                case BannerBlockEntity banner when banner.hasCustomName() && banner instanceof AccessorBannerBlockEntity accessor ->\n                        accessor.setName(TextHelper.applyFont(banner.getCustomName(), fontId));\n                default ->\n                        context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.font.unsupported_block\", tile.getBlockState().getBlock().getName()));\n            }\n        }\n        return 1;\n    }\n\n    private static UnaryOperator<SignText> applySignFont(ResourceLocation fontId) {\n        return text -> {\n            SignText newText = text;\n            for (int i = 0; i < 4; i++) {\n                newText = newText.setMessage(i, TextHelper.applyFont(text.getMessage(i, false).copy(), fontId));\n            }\n            return newText;\n        };\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/HandCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.DynamicOps;\nimport com.mojang.serialization.JsonOps;\nimport net.darkhax.bookshelf.common.api.commands.IEnumCommand;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.util.CommandHelper;\nimport net.darkhax.bookshelf.common.api.util.TextHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.nbt.NbtOps;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.entity.LivingEntity;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.Ingredient;\n\nimport java.util.Comparator;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.StringJoiner;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic enum HandCommand implements IEnumCommand {\n\n    ID((stack, level) -> TextHelper.copyText(Objects.requireNonNull(level.registryAccess().registryOrThrow(Registries.ITEM).getKey(stack.getItem())).toString())),\n    STRING((stack, level) -> TextHelper.copyText(stack.toString())),\n    INGREDIENT(json(MapCodecs.INGREDIENT.get(), (stack, level) -> Ingredient.of(stack))),\n    STACK_JSON(json(MapCodecs.ITEM_STACK.get(), (stack, level) -> stack)),\n    STACK_NBT(nbt(MapCodecs.ITEM_STACK.get(), (stack, level) -> stack)),\n    COMPONENTS((stack, level) -> {\n        final StringJoiner joiner = new StringJoiner(\"\\n\");\n        stack.getComponents().stream().sorted(Comparator.comparing(r -> r.type().toString())).forEach(component -> {\n            joiner.add(component.type() + \" = \" + unsafeEncode(Objects.requireNonNull(component.type().codec()), NbtOps.INSTANCE, component.value(), level));\n        });\n        return TextHelper.copyText(joiner.toString());\n    }),\n    TAGS(((stack, level) -> {\n        final StringJoiner joiner = new StringJoiner(\"\\n\");\n        stack.getTags().map(key -> key.location().toString()).sorted().forEach(joiner::add);\n        return TextHelper.copyText(joiner.toString());\n    }));\n\n    private final ItemFormat format;\n\n    HandCommand(ItemFormat format) {\n        this.format = format;\n    }\n\n    @Override\n    public int run(CommandContext<CommandSourceStack> context) {\n        final CommandSourceStack source = context.getSource();\n        if (source.getEntity() instanceof LivingEntity living) {\n            context.getSource().sendSuccess(() -> getFormattedResults(context.getSource().getLevel(), living.getMainHandItem()), false);\n        }\n        return Command.SINGLE_SUCCESS;\n    }\n\n    private Component getFormattedResults(ServerLevel level, ItemStack stack) {\n        if (stack.isEmpty()) {\n            return Component.translatable(\"commands.bookshelf.hand.error.not_air\").withStyle(ChatFormatting.RED);\n        }\n        try {\n            return this.format.formatItem(stack, level);\n        }\n        catch (Throwable e) {\n            Constants.LOG.error(\"Encountered an error when formatting item as {}.\", this.name(), e);\n        }\n        return Component.translatable(\"commands.bookshelf.hand.error.internal\").withStyle(ChatFormatting.RED);\n    }\n\n    private static <T> ItemFormat json(Codec<T> codec, BiFunction<ItemStack, ServerLevel, T> mapper) {\n        return fromCodec(JsonOps.INSTANCE, Constants.GSON_PRETTY::toJson, codec, mapper);\n    }\n\n    private static <T> ItemFormat nbt(Codec<T> codec, BiFunction<ItemStack, ServerLevel, T> mapper) {\n        return fromCodec(NbtOps.INSTANCE, Tag::toString, codec, mapper);\n    }\n\n    private static <T, D> ItemFormat fromCodec(DynamicOps<D> ops, Function<D, String> dataFormatter, Codec<T> codec, BiFunction<ItemStack, ServerLevel, T> mapper) {\n        return (stack, level) -> {\n            final T value = mapper.apply(stack, level);\n            final D data = codec.encodeStart(level.registryAccess().createSerializationContext(ops), value).getOrThrow();\n            return TextHelper.copyText(dataFormatter.apply(data));\n        };\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static <T> T unsafeEncode(Codec codec, DynamicOps<T> ops, Object input, ServerLevel level) {\n        return (T) codec.encodeStart(level.registryAccess().createSerializationContext(ops), input).getOrThrow();\n    }\n\n    @Override\n    public String getCommandName() {\n        return this.name().toLowerCase(Locale.ROOT);\n    }\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        return CommandHelper.buildFromEnum(\"hand\", HandCommand.class);\n    }\n\n    interface ItemFormat {\n        Component formatItem(ItemStack stack, ServerLevel level);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/RenameCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.commands.arguments.ComponentArgument;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.LivingEntity;\n\npublic class RenameCommand {\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        return Commands.literal(\"rename\").requires(PermissionLevel.GAMEMASTER).then(Commands.argument(\"new_name\", ComponentArgument.textComponent(context)).executes(ctx -> {\n            final Component newName = ComponentArgument.getComponent(ctx, \"new_name\");\n            if (ctx.getSource().getEntity() instanceof LivingEntity living && !living.getMainHandItem().isEmpty()) {\n                living.getMainHandItem().set(DataComponents.CUSTOM_NAME, newName);\n            }\n            return Command.SINGLE_SUCCESS;\n        }));\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/StructureCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport com.mojang.brigadier.context.CommandContext;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.commands.arguments.coordinates.BlockPosArgument;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.level.ChunkPos;\nimport net.minecraft.world.level.levelgen.structure.Structure;\n\nimport java.util.Set;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\npublic class StructureCommand {\n\n    public static LiteralArgumentBuilder<CommandSourceStack> build() {\n        final LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal(\"structure\").requires(PermissionLevel.GAMEMASTER);\n        root.executes(withPosition(StructureCommand::structureAt, ctx -> ctx.getSource().getEntity().blockPosition()));\n        root.then(Commands.argument(\"position\", BlockPosArgument.blockPos()).executes(withPosition(StructureCommand::structureAt, ctx -> BlockPosArgument.getBlockPos(ctx, \"position\"))));\n        return root;\n    }\n\n    private static Set<ResourceLocation> getUniqueStructuresAt(CommandContext<CommandSourceStack> context, BlockPos pos) {\n        final ServerLevel level = context.getSource().getLevel();\n        final Registry<Structure> registry = level.registryAccess().registryOrThrow(Registries.STRUCTURE);\n        return level.structureManager().startsForStructure(new ChunkPos(pos), s -> true).stream().filter(s -> s.getBoundingBox().isInside(pos)).map(s -> registry.getKey(s.getStructure())).collect(Collectors.toSet());\n    }\n\n    private static int structureAt(CommandContext<CommandSourceStack> context, BlockPos pos) {\n        final Set<ResourceLocation> structures = getUniqueStructuresAt(context, pos);\n        if (structures.isEmpty()) {\n            context.getSource().sendFailure(Component.translatable(\"commands.bookshelf.structure.error.no_structures\"));\n        }\n        else {\n            context.getSource().sendSuccess(() -> Component.translatable(\"commands.bookshelf.structure.found\", structures.size()).append(Component.literal(\"\\n  \" + structures.stream().map(ResourceLocation::toString).collect(Collectors.joining(\"\\n  \")))), false);\n        }\n        return structures.size();\n    }\n\n    private static Command<CommandSourceStack> withPosition(BiFunction<CommandContext<CommandSourceStack>, BlockPos, Integer> cmd, Function<CommandContext<CommandSourceStack>, BlockPos> provider) {\n        return ctx -> cmd.apply(ctx, provider.apply(ctx));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/command/TranslateCommand.java",
    "content": "package net.darkhax.bookshelf.common.impl.command;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.arguments.StringArgumentType;\nimport com.mojang.brigadier.builder.LiteralArgumentBuilder;\nimport net.darkhax.bookshelf.common.api.commands.PermissionLevel;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.network.chat.Component;\n\npublic class TranslateCommand {\n    public static LiteralArgumentBuilder<CommandSourceStack> build(CommandBuildContext context) {\n        return Commands.literal(\"translate\").requires(PermissionLevel.GAMEMASTER).then(Commands.argument(\"key\", StringArgumentType.word()).executes(ctx -> {\n            ctx.getSource().sendSuccess(() -> Component.translatable(StringArgumentType.getString(ctx, \"key\")), false);\n            return Command.SINGLE_SUCCESS;\n        }));\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/And.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.List;\n\n/**\n * This load condition will test an array of sub-conditions and make sure all of them are met.\n */\npublic class And implements ILoadCondition {\n\n    public static final ResourceLocation TYPE_ID = Constants.id(\"and\");\n    public static final CachedSupplier<ConditionType> TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID));\n    public static final MapCodec<And> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList(\"conditions\", And::getConditions)).apply(instance, And::new));\n\n    private final List<ILoadCondition> conditions;\n\n    private And(List<ILoadCondition> conditions) {\n\n        this.conditions = conditions;\n    }\n\n    public List<ILoadCondition> getConditions() {\n\n        return this.conditions;\n    }\n\n    @Override\n    public boolean allowLoading() {\n        for (ILoadCondition condition : this.conditions) {\n            if (!condition.allowLoading()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return TYPE.get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/ModLoaded.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.Set;\n\n/**\n * This load condition will test that an array of mod IDs are all loaded.\n */\npublic class ModLoaded implements ILoadCondition {\n\n    public static final ResourceLocation TYPE_ID = Constants.id(\"mod_loaded\");\n    public static final CachedSupplier<ConditionType> TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID));\n    public static final MapCodec<ModLoaded> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.STRING.getSet(\"values\", ModLoaded::getRequiredMods)).apply(instance, ModLoaded::new));\n\n    private final Set<String> requiredMods;\n\n    private ModLoaded(Set<String> requiredMods) {\n\n        this.requiredMods = requiredMods;\n    }\n\n    @Override\n    public boolean allowLoading() {\n        for (String modId : this.requiredMods) {\n            if (!Services.PLATFORM.isModLoaded(modId)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public Set<String> getRequiredMods() {\n        return this.requiredMods;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return TYPE.get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/Not.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.List;\n\n/**\n * This load condition will test an array of sub-conditions and make sure none of them are met.\n */\npublic class Not implements ILoadCondition {\n\n    public static final ResourceLocation TYPE_ID = Constants.id(\"not\");\n    public static final CachedSupplier<ConditionType> TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID));\n    public static final MapCodec<Not> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList(\"conditions\", Not::getConditions)).apply(instance, Not::new));\n\n    private final List<ILoadCondition> conditions;\n\n    private Not(List<ILoadCondition> conditions) {\n        this.conditions = conditions;\n    }\n\n    public List<ILoadCondition> getConditions() {\n        return this.conditions;\n    }\n\n    @Override\n    public boolean allowLoading() {\n        for (ILoadCondition condition : this.conditions) {\n            if (condition.allowLoading()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return TYPE.get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/OnPlatform.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\n/**\n * This load condition will test the current mod loading platform.\n */\npublic class OnPlatform implements ILoadCondition {\n\n    public static final ResourceLocation TYPE_ID = Constants.id(\"on_platform\");\n    public static final CachedSupplier<ConditionType> TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID));\n    public static final MapCodec<OnPlatform> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.STRING.get(\"platform\", OnPlatform::getRequiredPlatform)).apply(instance, OnPlatform::new));\n\n    private final String requiredPlatform;\n\n    private OnPlatform(String requiredPlatform) {\n        this.requiredPlatform = requiredPlatform;\n    }\n\n    @Override\n    public boolean allowLoading() {\n        return Services.PLATFORM.getName().equalsIgnoreCase(this.getRequiredPlatform());\n    }\n\n    public String getRequiredPlatform() {\n        return this.requiredPlatform;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return TYPE.get();\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/Or.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.List;\n\n/**\n * This load condition will test an array of sub-conditions and make sure at least one of them are met.\n */\npublic class Or implements ILoadCondition {\n\n    public static final ResourceLocation TYPE_ID = Constants.id(\"or\");\n    public static final CachedSupplier<ConditionType> TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID));\n    public static final MapCodec<Or> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList(\"conditions\", Or::getConditions)).apply(instance, Or::new));\n\n    private final List<ILoadCondition> conditions;\n\n    private Or(List<ILoadCondition> conditions) {\n        this.conditions = conditions;\n    }\n\n    public List<ILoadCondition> getConditions() {\n        return this.conditions;\n    }\n\n    @Override\n    public boolean allowLoading() {\n        for (ILoadCondition condition : this.conditions) {\n            if (condition.allowLoading()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return TYPE.get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/RegistryContains.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.conditions;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.conditions.ConditionType;\nimport net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.Set;\n\npublic class RegistryContains<T> implements ILoadCondition {\n\n    public static final ResourceLocation BLOCK = Constants.id(\"block_exists\");\n    public static final ResourceLocation ITEM = Constants.id(\"item_exists\");\n    public static final ResourceLocation ENTITY = Constants.id(\"entity_exists\");\n    public static final ResourceLocation BLOCK_ENTITY = Constants.id(\"block_entity_exists\");\n\n    private final Registry<T> registry;\n    private final Set<ResourceLocation> requiredIds;\n    private final CachedSupplier<ConditionType> type;\n\n\n    public static <RT> MapCodec<RegistryContains<RT>> of(ResourceLocation typeId, Registry<RT> registry) {\n        return RecordCodecBuilder.mapCodec(instance -> instance.group(\n                MapCodecs.RESOURCE_LOCATION.getSet(\"values\", RegistryContains::getRequiredEntries)\n        ).apply(instance, requiredEntries -> new RegistryContains<>(typeId, registry, requiredEntries)));\n    }\n\n    private RegistryContains(ResourceLocation typeId, Registry<T> registry, Set<ResourceLocation> requiredIds) {\n        this.registry = registry;\n        this.requiredIds = requiredIds;\n        this.type = CachedSupplier.cache(() -> LoadConditions.getType(typeId));\n    }\n\n    @Override\n    public boolean allowLoading() {\n        for (ResourceLocation id : this.requiredIds) {\n            if (!this.registry.containsKey(id)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    public Set<ResourceLocation> getRequiredEntries() {\n        return this.requiredIds;\n    }\n\n    @Override\n    public ConditionType getType() {\n        return this.type.get();\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/criterion/item/NamespaceItemPredicate.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.criterion.item;\n\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.minecraft.advancements.critereon.ItemSubPredicate;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.item.ItemStack;\n\npublic record NamespaceItemPredicate(String namespace) implements ItemSubPredicate {\n\n    public static final Codec<NamespaceItemPredicate> CODEC = RecordCodecBuilder.create(instance -> instance.group(MapCodecs.STRING.get(\"namespace\", NamespaceItemPredicate::namespace)).apply(instance, NamespaceItemPredicate::new));\n\n    @Override\n    public boolean matches(ItemStack stack) {\n        return namespace.equals(BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace());\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/criterion/trigger/AdvancementTrigger.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.criterion.trigger;\n\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.minecraft.advancements.AdvancementHolder;\nimport net.minecraft.advancements.critereon.ContextAwarePredicate;\nimport net.minecraft.advancements.critereon.EntityPredicate;\nimport net.minecraft.advancements.critereon.SimpleCriterionTrigger;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.Optional;\nimport java.util.Set;\n\npublic class AdvancementTrigger extends SimpleCriterionTrigger<AdvancementTrigger.Instance> {\n\n    public static final AdvancementTrigger TRIGGER = new AdvancementTrigger();\n    private static final Codec<AdvancementTrigger.Instance> CODEC = RecordCodecBuilder.create(instance -> instance.group(\n            EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf(\"player\").forGetter(Instance::player),\n            MapCodecs.RESOURCE_LOCATION.getSet(\"advancements\", Instance::advancementIds)\n    ).apply(instance, Instance::new));\n\n    @Override\n    @NotNull\n    public Codec<AdvancementTrigger.Instance> codec() {\n        return CODEC;\n    }\n\n    public void trigger(ServerPlayer player, AdvancementHolder advancement) {\n        this.trigger(player, instance -> instance.advancementIds().contains(advancement.id()));\n    }\n\n    public record Instance(Optional<ContextAwarePredicate> player, Set<ResourceLocation> advancementIds) implements SimpleCriterionTrigger.SimpleInstance {\n\n        @Override\n        @NotNull\n        public Optional<ContextAwarePredicate> player() {\n            return this.player;\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/AllOfIngredient.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.ingredient;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.Ingredient;\n\nimport java.util.List;\n\npublic class AllOfIngredient implements IngredientLogic<AllOfIngredient> {\n\n    public static final MapCodec<AllOfIngredient> CODEC = MapCodecs.flexibleList(Ingredient.CODEC).xmap(AllOfIngredient::new, i -> i.ingredients).fieldOf(\"ingredients\");\n    public static final StreamCodec<RegistryFriendlyByteBuf, AllOfIngredient> STREAM = StreamCodecs.list(StreamCodecs.INGREDIENT_NON_EMPTY).map(AllOfIngredient::new, v -> v.ingredients);\n\n    private final List<Ingredient> ingredients;\n\n    public AllOfIngredient(List<Ingredient> ingredients) {\n        this.ingredients = ingredients;\n    }\n\n    @Override\n    public boolean test(ItemStack stack) {\n        for (Ingredient ingredient : this.ingredients) {\n            if (!ingredient.test(stack)) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/BlockTagIngredient.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.ingredient;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class BlockTagIngredient implements IngredientLogic<BlockTagIngredient> {\n\n    public static final MapCodec<BlockTagIngredient> CODEC = MapCodecs.flexibleList(TagKey.codec(Registries.BLOCK)).xmap(BlockTagIngredient::new, l -> l.blockTags).fieldOf(\"tag\");\n    public static final StreamCodec<RegistryFriendlyByteBuf, BlockTagIngredient> STREAM = StreamCodec.of(\n            (buf, val) -> buf.writeCollection(val.blockTags, (b1, tag) -> {\n                b1.writeResourceLocation(tag.location());\n            }),\n            buf -> new BlockTagIngredient(buf.readCollection(ArrayList::new, b1 -> TagKey.create(Registries.BLOCK, b1.readResourceLocation())))\n    );\n\n    private final List<TagKey<Block>> blockTags;\n    private List<ItemStack> matches;\n\n    public BlockTagIngredient(List<TagKey<Block>> blockTags) {\n        this.blockTags = blockTags;\n    }\n\n    @Override\n    public List<ItemStack> getAllMatchingStacks() {\n        if (this.matches == null) {\n            this.matches = new ArrayList<>();\n            for (TagKey<Block> tag : blockTags) {\n                for (Holder<Block> entry : BuiltInRegistries.BLOCK.getTagOrEmpty(tag)) {\n                    final ItemStack stack = new ItemStack(entry.value());\n                    if (!stack.isEmpty()) {\n                        this.matches.add(stack);\n                    }\n                }\n            }\n        }\n        return this.matches;\n    }\n\n    @Override\n    public boolean test(ItemStack stack) {\n        if (stack == null || stack.isEmpty()) {\n            return false;\n        }\n        for (ItemStack valid : this.getAllMatchingStacks()) {\n            if (valid.getItem() == stack.getItem()) {\n                return true;\n            }\n        }\n        return false;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/EitherIngredient.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.ingredient;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.Ingredient;\n\nimport java.util.List;\n\npublic class EitherIngredient implements IngredientLogic<EitherIngredient> {\n\n    public static final MapCodec<EitherIngredient> CODEC = MapCodecs.flexibleList(Ingredient.CODEC).xmap(EitherIngredient::new, i -> i.ingredients).fieldOf(\"ingredients\");\n    public static final StreamCodec<RegistryFriendlyByteBuf, EitherIngredient> STREAM = StreamCodecs.list(StreamCodecs.INGREDIENT_NON_EMPTY).map(EitherIngredient::new, v -> v.ingredients);\n\n    private final List<Ingredient> ingredients;\n\n    public EitherIngredient(List<Ingredient> ingredients) {\n        this.ingredients = ingredients;\n    }\n\n    @Override\n    public boolean test(ItemStack stack) {\n        for (Ingredient ingredient : this.ingredients) {\n            if (ingredient.test(stack)) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/FalseIngredient.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.ingredient;\n\nimport com.google.gson.JsonObject;\nimport com.mojang.serialization.JsonOps;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.Ingredient;\n\npublic class FalseIngredient implements IngredientLogic<FalseIngredient> {\n\n    public static final FalseIngredient SINGLETON = new FalseIngredient();\n    public static final MapCodec<FalseIngredient> CODEC = MapCodec.unit(SINGLETON);\n    public static final StreamCodec<RegistryFriendlyByteBuf, FalseIngredient> STREAM = StreamCodec.unit(SINGLETON);\n    public static final CachedSupplier<Ingredient> INSTANCE = CachedSupplier.cache(() -> {\n        final JsonObject obj = new JsonObject();\n        obj.addProperty(\"fabric:type\", \"bookshelf:false\");\n        obj.addProperty(\"tag\", \"bookshelf:false\");\n        return Ingredient.CODEC.decode(JsonOps.INSTANCE, obj).getOrThrow().getFirst();\n    });\n\n    @Override\n    public boolean test(ItemStack stack) {\n        return false;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/ModIdIngredient.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.ingredient;\n\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.List;\n\npublic class ModIdIngredient implements IngredientLogic<ModIdIngredient> {\n\n    public static final MapCodec<ModIdIngredient> CODEC = MapCodecs.flexibleList(Codec.STRING).xmap(ModIdIngredient::new, i -> i.modIds).fieldOf(\"mod\");\n    public static final StreamCodec<RegistryFriendlyByteBuf, ModIdIngredient> STREAM = StreamCodecs.list(StreamCodecs.STRING).map(ModIdIngredient::new, i -> i.modIds);\n\n    private final List<String> modIds;\n\n    public ModIdIngredient(List<String> modIds) {\n        this.modIds = modIds;\n    }\n\n    @Override\n    public boolean test(ItemStack stack) {\n        final String owner = BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace();\n        for (String id : this.modIds) {\n            if (owner.equals(id)) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/entries/LootItemStack.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.loot.entries;\n\nimport com.mojang.serialization.MapCodec;\nimport com.mojang.serialization.codecs.RecordCodecBuilder;\nimport net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.storage.loot.LootContext;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolSingletonContainer;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunction;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemCondition;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * A LootPool entry type that produces copies of predefined ItemStack.\n */\npublic class LootItemStack extends LootPoolSingletonContainer {\n\n    private static final Supplier<LootPoolEntryType> TYPE = CachedSupplier.of(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE, Constants.id(\"item_stack\"));\n    public static final MapCodec<LootItemStack> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.ITEM_STACK.get(\"item\", LootItemStack::getBaseStack))\n            .and(singletonFields(instance))\n            .apply(instance, LootItemStack::new)\n    );\n\n    private final ItemStack baseStack;\n\n    private LootItemStack(ItemStack baseStack, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {\n        super(weight, quality, conditions, functions);\n        this.baseStack = baseStack;\n    }\n\n    public ItemStack getBaseStack() {\n        return this.baseStack.copy();\n    }\n\n    @Override\n    protected void createItemStack(Consumer<ItemStack> consumer, @NotNull LootContext context) {\n        consumer.accept(this.baseStack.copy());\n    }\n\n    @NotNull\n    @Override\n    public LootPoolEntryType getType() {\n        return TYPE.get();\n    }\n\n    public static LootItemStack of(ItemStack baseStack, int weight) {\n        return new LootItemStack(baseStack, weight, 0, List.of(), List.of());\n    }\n\n    public static LootItemStack of(ItemStack baseStack, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {\n        return new LootItemStack(baseStack, weight, quality, conditions, functions);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/FingerprintCodec.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.loot.modifiers;\n\nimport com.google.gson.JsonElement;\nimport com.mojang.datafixers.util.Pair;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.DynamicOps;\n\n/**\n * A codec wrapper that adds functionality to compute and set a fingerprint hash for certain objects during decoding.\n * This is achieved by hashing the JSON representation of the input.\n * <p>\n * This implementation is only intended for the LootPool codec. Loot pools do not have their own identity, so we use\n * this hash to keep track of their position within a LootTable and to detect if a user has overwritten the input.\n *\n * @param <T> The type for the codec to serialize.\n */\npublic class FingerprintCodec<T> implements Codec<T> {\n\n    private final Codec<T> delegate;\n\n    public FingerprintCodec(Codec<T> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public <T1> DataResult<Pair<T, T1>> decode(DynamicOps<T1> ops, T1 input) {\n        final DataResult<Pair<T, T1>> result = this.delegate.decode(ops, input);\n        if (input instanceof JsonElement json && result.error().isEmpty()) {\n            result.result().ifPresent(r -> {\n                if (r.getFirst() instanceof ILootPoolHooks hooks) {\n                    hooks.bookshelf$setHash(json.toString().hashCode());\n                }\n            });\n        }\n        return result;\n    }\n\n    @Override\n    public <T1> DataResult<T1> encode(T input, DynamicOps<T1> ops, T1 prefix) {\n        return this.delegate.encode(input, ops, prefix);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/ILootPoolHooks.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.loot.modifiers;\n\nimport org.jetbrains.annotations.Nullable;\n\n/**\n * Internal hooks related to loot pools.\n */\npublic interface ILootPoolHooks {\n\n    void bookshelf$setHash(int fingerprint);\n\n    @Nullable\n    Integer bookshelf$getHash();\n\n    default boolean bookshelf$matches(int toMatch) {\n        final Integer hash = this.bookshelf$getHash();\n        return hash != null && hash == toMatch;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/LootModificationHandler.java",
    "content": "package net.darkhax.bookshelf.common.impl.data.loot.modifiers;\n\nimport net.darkhax.bookshelf.common.api.data.loot.modifiers.LootPoolAddition;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootPoolAdditionAdapter;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootPool;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * Handles collecting loot pool modifications from various mods and then applying those modifications to loot tables.\n */\npublic class LootModificationHandler {\n\n    public static final Supplier<LootModificationHandler> HANDLER = CachedSupplier.cache(() -> {\n        final LootModificationHandler handler = new LootModificationHandler();\n        Services.CONTENT.get().forEach(provider -> provider.defineLootPoolAdditions(new LootPoolAdditionAdapter(provider.namespace(), handler::addPoolEntry)));\n        return handler;\n    });\n\n    private final Map<ResourceLocation, Map<Integer, Map<Integer, List<LootPoolAddition>>>> newPoolEntries = new HashMap<>();\n\n    private void addPoolEntry(ResourceLocation tableId, int poolIndex, int poolHash, LootPoolAddition entry) {\n        // We consolidate entries together here to avoid additional iterations later on.\n        // This also helps us avoid making the list mutable and immutable many times.\n        final Map<Integer, Map<Integer, List<LootPoolAddition>>> tableEntries = newPoolEntries.computeIfAbsent(tableId, k -> new LinkedHashMap<>());\n        final Map<Integer, List<LootPoolAddition>> indexEntries = tableEntries.computeIfAbsent(poolIndex, k -> new LinkedHashMap<>());\n        final List<LootPoolAddition> hashEntries = indexEntries.computeIfAbsent(poolHash, k -> new LinkedList<>());\n        hashEntries.add(entry);\n    }\n\n    public void processLootTable(ResourceLocation tableId, LootTable table) {\n        if (newPoolEntries.containsKey(tableId) && table instanceof AccessorLootTable accessor) {\n            final List<LootPool> pools = accessor.bookshelf$pools();\n            for (Map.Entry<Integer, Map<Integer, List<LootPoolAddition>>> indexEntry : newPoolEntries.get(tableId).entrySet()) {\n                for (Map.Entry<Integer, List<LootPoolAddition>> hashEntry : indexEntry.getValue().entrySet()) {\n                    final LootPool targetPool = findPool(indexEntry.getKey(), hashEntry.getKey(), pools);\n                    if (targetPool == null) {\n                        Constants.LOG.warn(\"Could not locate pool {} in table '{}'. The following loot additions will not be applied. {}\", hashEntry.getKey(), tableId, hashEntry.getValue().stream().map(a -> a.id().toString()).collect(Collectors.joining(\", \")));\n                    }\n                    else if (targetPool instanceof AccessorLootPool pool) {\n                        final List<LootPoolEntryContainer> entries = new LinkedList<>(pool.bookshelf$entries());\n                        for (LootPoolAddition addition : hashEntry.getValue()) {\n                            entries.add(addition.entry());\n                            Constants.LOG.debug(\"Added entry `{}` to pool `{}` in table `{}`.\", addition.id(), indexEntry.getKey(), tableId);\n                        }\n                        pool.bookshelf$setEntries(Collections.unmodifiableList(entries));\n                    }\n                }\n            }\n        }\n    }\n\n    @Nullable\n    private static LootPool findPool(int targetIndex, int targetHash, List<LootPool> pools) {\n        // Check the target first, which will usually be correct.\n        if (targetIndex > -1 && targetIndex < pools.size() + 1) {\n            final LootPool pool = pools.get(targetIndex);\n            if (pool instanceof ILootPoolHooks hooks && hooks.bookshelf$matches(targetHash)) {\n                return pool;\n            }\n        }\n        // If the pool order has been changed we can not rely on the target index, however\n        // the hash can still be used as long as there is only one match.\n        final List<LootPool> matchingHashes = new ArrayList<>();\n        for (final LootPool pool : pools) {\n            if (pool instanceof ILootPoolHooks hooks && hooks.bookshelf$matches(targetHash)) {\n                matchingHashes.add(pool);\n            }\n        }\n        return matchingHashes.size() == 1 ? matchingHashes.getFirst() : null;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/recipe/RecipeTypeImpl.java",
    "content": "package net.darkhax.bookshelf.common.impl.recipe;\n\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.crafting.Recipe;\nimport net.minecraft.world.item.crafting.RecipeType;\nimport org.jetbrains.annotations.NotNull;\n\npublic record RecipeTypeImpl<T extends Recipe<?>>(ResourceLocation id) implements RecipeType<T> {\n\n    @NotNull\n    @Override\n    public String toString() {\n        return this.id.toString();\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockEntityRendererAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\n\nimport java.util.function.BiConsumer;\n\n@SuppressWarnings(\"rawtypes\")\npublic record BlockEntityRendererAdapter(BiConsumer<BlockEntityType, BlockEntityRendererProvider> bindFunc) {\n\n    public <T extends BlockEntity> void bind(BlockEntityType<T> type, BlockEntityRendererProvider<T> rendererProvider) {\n        this.bindFunc.accept(type, rendererProvider);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockRegistryAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.BlockItem;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * A registry adapter for the block registry.\n */\npublic class BlockRegistryAdapter extends GameRegistryAdapter<Block> {\n\n    public BlockRegistryAdapter(RegistrationContext context, ResourceKey<Registry<Block>> regKey, BiConsumer<ResourceKey<Block>, Supplier<Block>> registryFunc) {\n        super(context, regKey, registryFunc);\n    }\n\n    /**\n     * Adds a new block to the block registry and queues up a BlockItem to be registered automatically during item\n     * registration. The item will be registered using the same ID as the block.\n     *\n     * @param key   The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param value A supplier that will produce the block to register.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceKey<Block>, Block> addPlaceable(String key, Supplier<Block> value) {\n        return this.addPlaceable(key, value, block -> new BlockItem(block, new Item.Properties()));\n    }\n\n    /**\n     * Adds a new block to the block registry and queues up a custom placer item to be registered automatically during\n     * item registry. The item will be registered using the same ID as the block.\n     *\n     * @param key    The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param value  A supplier that will produce the block to register.\n     * @param placer A factory that produces the placer item. The input block is the block that was registered.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceKey<Block>, Block> addPlaceable(String key, Supplier<Block> value, Function<Block, Item> placer) {\n        final RegistryReference<ResourceKey<Block>, Block> reference = this.add(key, value);\n        this.context.addPlaceableBlock(reference, placer);\n        return reference;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockRenderTypeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.function.BiConsumer;\n\npublic record BlockRenderTypeAdapter(BiConsumer<Block, RenderType> bindFunc) {\n\n    public void add(Block block, RenderType type) {\n        this.bindFunc.accept(block, type);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/CommandArgumentAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class CommandArgumentAdapter extends GenericRegistryAdapter<CommandArgumentAdapter.TypeInfo<?>> {\n\n    public CommandArgumentAdapter(RegistrationContext context, BiConsumer<ResourceLocation, Supplier<TypeInfo<?>>> registryFunc) {\n        super(context, registryFunc);\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    public <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void add(String id, Class argumentClass, ArgumentTypeInfo<A, T> info) {\n        this.add(id, new TypeInfo<>(argumentClass, info));\n    }\n\n    public record TypeInfo<A extends ArgumentType<?>>(Class<? extends ArgumentType<?>> argType, ArgumentTypeInfo<A, ?> typeIfo) {\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/CreativeModeTabAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.minecraft.core.Registry;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\npublic class CreativeModeTabAdapter extends GameRegistryAdapter<CreativeModeTab> {\n\n    public CreativeModeTabAdapter(RegistrationContext context, ResourceKey<Registry<CreativeModeTab>> regKey, BiConsumer<ResourceKey<CreativeModeTab>, Supplier<CreativeModeTab>> registryFunc) {\n        super(context, regKey, registryFunc);\n    }\n\n    /**\n     * Adds a new creative mode tab to the game. The title will be based on the registry ID of the tab.\n     *\n     * @param key     The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param icon    An item to display as the icon for the tab.\n     * @param display Generates the items to display in the tab.\n     */\n    public void add(String key, Supplier<ItemStack> icon, CreativeModeTab.DisplayItemsGenerator display) {\n        this.add(key, builder -> {\n            builder.title(Component.translatable(\"itemGroup.\" + this.context.namespace() + \".\" + key));\n            builder.icon(icon);\n            builder.displayItems(display);\n        });\n    }\n\n    /**\n     * Adds a new creative mode tab to the game.\n     *\n     * @param key         The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param builderFunc A creative mode tab builder.\n     */\n    public void add(String key, Consumer<CreativeModeTab.Builder> builderFunc) {\n        final CreativeModeTab.Builder builder = Services.GAMEPLAY.tabBuilder();\n        builderFunc.accept(builder);\n        this.add(key, builder.build());\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/IngredientTypeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.ResourceLocation;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\n/**\n * A registry adapter that can register new types of ingredients.\n */\n@SuppressWarnings(\"rawtypes\")\npublic class IngredientTypeAdapter extends GenericRegistryAdapter<IngredientTypeAdapter.IngredientType> {\n\n    public IngredientTypeAdapter(RegistrationContext context, BiConsumer<ResourceLocation, Supplier<IngredientType>> registryFunc) {\n        super(context, registryFunc);\n    }\n\n    /**\n     * Adds a new type of ingredient to the game.\n     *\n     * @param key    The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param codec  A map codec that constructs the ingredient logic from map data like JSON.\n     * @param stream A ByteBuf codec that constructs the ingredient logic from network data.\n     * @param <T>    The type of the ingredient logic.\n     * @return A reference to the registry entry.\n     */\n    public <T extends IngredientLogic<T>> RegistryReference<ResourceLocation, IngredientType> add(String key, MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> stream) {\n        return this.add(key, new IngredientType<>(codec, stream));\n    }\n\n    /**\n     * An internal type that holds a map codec and the ByteBuf codec for a custom ingredient type.\n     *\n     * @param codec  A codec that reads the ingredient from map data, like JSON.\n     * @param stream A ByteBuf codec that reads the ingredient from network data.\n     * @param <T>    The type of the custom ingredient logic.\n     */\n    public record IngredientType<T extends IngredientLogic<T>>(MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> stream) {\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootDescriptionAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriber;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;\n\nimport java.util.function.BiConsumer;\n\npublic record LootDescriptionAdapter(BiConsumer<LootPoolEntryType, LootPoolEntryDescriber> registryFunc) {\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootEntryTypeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class LootEntryTypeAdapter extends GameRegistryAdapter<LootPoolEntryType> {\n    public LootEntryTypeAdapter(RegistrationContext context, ResourceKey<Registry<LootPoolEntryType>> registry, BiConsumer<ResourceKey<LootPoolEntryType>, Supplier<LootPoolEntryType>> registryFunc) {\n        super(context, registry, registryFunc);\n    }\n\n    public <T extends LootPoolEntryContainer> void add(String key, MapCodec<T> codec) {\n        this.add(key, new LootPoolEntryType(codec));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootPoolAdditionAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.data.loot.PoolTarget;\nimport net.darkhax.bookshelf.common.api.data.loot.modifiers.LootPoolAddition;\nimport net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack;\nimport net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootItem;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\n\nimport java.util.List;\n\n/**\n * Registers new LootPoolAddition from various mods to be applied by Bookshelf.\n *\n * @param owner        The ID of the mod registering loot additions.\n * @param registerFunc The function used to register new additions.\n */\npublic record LootPoolAdditionAdapter(String owner, RegisterFunc registerFunc) {\n\n    public void add(String id, PoolTarget pool, ItemStack item, int weight) {\n        add(id, pool.table(), pool.index(), pool.hash(), item, weight);\n    }\n\n    public void add(String id, PoolTarget pool, Item item, int weight) {\n        add(id, pool.table(), pool.index(), pool.hash(), item, weight);\n    }\n\n    public void add(String id, ResourceKey<LootTable> tableId, int poolIndex, int poolHash, ItemStack item, int weight) {\n        add(id, tableId.location(), poolIndex, poolHash, item, weight);\n    }\n\n    public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, ItemStack item, int weight) {\n        add(id, tableId, poolIndex, poolHash, LootItemStack.of(item, weight));\n    }\n\n    public void add(String id, ResourceKey<LootTable> tableId, int poolIndex, int poolHash, Item item, int weight) {\n        add(id, tableId.location(), poolIndex, poolHash, item, weight);\n    }\n\n    public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, Item item, int weight) {\n        add(id, tableId, poolIndex, poolHash, AccessorLootItem.bookshelf$create(item.builtInRegistryHolder(), weight, 0, List.of(), List.of()));\n    }\n\n    public void add(String id, PoolTarget pool, LootPoolEntryContainer addition) {\n        add(id, pool.table(), pool.index(), pool.hash(), addition);\n    }\n\n    public void add(String id, ResourceKey<LootTable> tableId, int poolIndex, int poolHash, LootPoolEntryContainer addition) {\n        add(id, tableId.location(), poolIndex, poolHash, addition);\n    }\n\n    public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, LootPoolEntryContainer addition) {\n        registerFunc.register(tableId, poolIndex, poolHash, new LootPoolAddition(id(id), addition));\n    }\n\n    private ResourceLocation id(String id) {\n        return ResourceLocation.fromNamespaceAndPath(this.owner, id);\n    }\n\n    @FunctionalInterface\n    public interface RegisterFunc {\n        void register(ResourceLocation tableId, int poolIndex, int poolHash, LootPoolAddition addition);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/MenuScreenAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.MenuAccess;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.MenuType;\n\nimport java.util.function.BiConsumer;\n\n@SuppressWarnings(\"rawtypes\")\npublic record MenuScreenAdapter(BiConsumer<MenuType, ScreenFactory> func) {\n\n    public <M extends AbstractContainerMenu, U extends Screen & MenuAccess<M>> void bind(MenuType<? extends M> type, ScreenFactory<M, U> factory) {\n        func.accept(type, factory);\n    }\n\n    @FunctionalInterface\n    public interface ScreenFactory<T extends AbstractContainerMenu, U extends Screen & MenuAccess<T>> {\n        U create(T menu, Inventory playerInv, Component title);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/MenuTypeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class MenuTypeAdapter extends GenericRegistryAdapter<MenuTypeAdapter.ClientMenuFactory<? extends AbstractContainerMenu>> {\n\n    public MenuTypeAdapter(RegistrationContext context, BiConsumer<ResourceLocation, Supplier<ClientMenuFactory<? extends AbstractContainerMenu>>> registryFunc) {\n        super(context, registryFunc);\n    }\n\n    public interface ClientMenuFactory<T extends AbstractContainerMenu> {\n        T create(int containerId, Inventory playerInventory);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PacketAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.network.IPacket;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\n\nimport java.util.function.Consumer;\n\npublic record PacketAdapter(RegistrationContext context, Consumer<IPacket<?>> registerFunc) {\n    public <T extends CustomPacketPayload> IPacket<T> add(IPacket<T> packet) {\n        registerFunc.accept(packet);\n        return packet;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PotPatternAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.block.entity.DecoratedPotPattern;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\n/**\n * A registry adapter for decorated pot patterns like sherds.\n */\npublic final class PotPatternAdapter extends GameRegistryAdapter<DecoratedPotPattern> {\n\n    public PotPatternAdapter(RegistrationContext context, ResourceKey<Registry<DecoratedPotPattern>> regKey, BiConsumer<ResourceKey<DecoratedPotPattern>, Supplier<DecoratedPotPattern>> registryFunc) {\n        super(context, regKey, registryFunc);\n    }\n\n    /**\n     * Adds a new decorated pot pattern to the game registry.\n     *\n     * @param key The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceKey<DecoratedPotPattern>, DecoratedPotPattern> add(String key) {\n        return this.add(key, () -> new DecoratedPotPattern(this.id(key)));\n    }\n\n    /**\n     * Adds a new decorated pot pattern to the game registry and associates it with an item.\n     *\n     * @param key  The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param item The item to associate the pattern with.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceKey<DecoratedPotPattern>, DecoratedPotPattern> addWithItem(String key, Item item) {\n        final RegistryReference<ResourceKey<DecoratedPotPattern>, DecoratedPotPattern> pattern = this.add(key);\n        this.context.addPotPatternItem(item, pattern.key());\n        return pattern;\n    }\n\n    /**\n     * Adds a new decorated pot pattern to the game registry and associates it with an item.\n     *\n     * @param key  The ID to register the value under. This ID only needs to be unique within your namespace.\n     * @param item The item to associate the pattern with.\n     * @return A reference to the registry entry.\n     */\n    public RegistryReference<ResourceKey<DecoratedPotPattern>, DecoratedPotPattern> addWithItem(String key, Supplier<Item> item) {\n        return this.addWithItem(key, item.get());\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PotionBrewAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\npublic class PotionBrewAdapter {\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/RecipeTypeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.RegistryReference;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.recipe.RecipeTypeImpl;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.crafting.RecipeType;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class RecipeTypeAdapter extends GameRegistryAdapter<RecipeType<?>> {\n\n    public RecipeTypeAdapter(RegistrationContext context, ResourceKey<Registry<RecipeType<?>>> regKey, BiConsumer<ResourceKey<RecipeType<?>>, Supplier<RecipeType<?>>> registryFunc) {\n        super(context, regKey, registryFunc);\n    }\n\n    public RegistryReference<ResourceKey<RecipeType<?>>, RecipeType<?>> add(String key) {\n        return this.add(key, () -> new RecipeTypeImpl<>(ResourceLocation.fromNamespaceAndPath(context.namespace(), key)));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/SoundEventAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.sounds.SoundEvent;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class SoundEventAdapter extends GameRegistryAdapter<SoundEvent> {\n\n    public SoundEventAdapter(RegistrationContext context, ResourceKey<Registry<SoundEvent>> registryKey, BiConsumer<ResourceKey<SoundEvent>, Supplier<SoundEvent>> registryFunc) {\n        super(context, registryKey, registryFunc);\n    }\n\n    public void fixedRange(String id, float range) {\n        this.add(id, SoundEvent.createFixedRangeEvent(this.id(id), range));\n    }\n\n    public void variableRange(String id) {\n        this.add(id, SoundEvent.createVariableRangeEvent(this.id(id)));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/VillagerTradeAdapter.java",
    "content": "package net.darkhax.bookshelf.common.impl.registry.adapter;\n\nimport com.google.common.collect.ArrayListMultimap;\nimport com.google.common.collect.Multimap;\nimport net.darkhax.bookshelf.common.api.entity.villager.MerchantTier;\nimport net.minecraft.world.entity.npc.VillagerProfession;\nimport net.minecraft.world.entity.npc.VillagerTrades;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class VillagerTradeAdapter {\n\n    private final Map<VillagerProfession, Multimap<Integer, VillagerTrades.ItemListing>> villagerTrades = new HashMap<>();\n    private final List<VillagerTrades.ItemListing> rareTrades = new ArrayList<>();\n    private final List<VillagerTrades.ItemListing> commonTrades = new ArrayList<>();\n\n    /**\n     * Adds a new villager trade to the game.\n     *\n     * @param profession The profession that offers the trade.\n     * @param tier       The tier of that profession that offers the trade.\n     * @param trade      The trade to offer.\n     */\n    public void addTrade(VillagerProfession profession, int tier, VillagerTrades.ItemListing trade) {\n        villagerTrades.computeIfAbsent(profession, p -> ArrayListMultimap.create()).put(tier, trade);\n    }\n\n    /**\n     * Adds a new villager trade to the game.\n     *\n     * @param profession The profession that offers the trade.\n     * @param tier       The tier of that profession that offers the trade.\n     * @param trade      The trade to offer.\n     */\n    public void addTrade(VillagerProfession profession, MerchantTier tier, VillagerTrades.ItemListing trade) {\n        this.addTrade(profession, tier.ordinal() + 1, trade);\n    }\n\n    /**\n     * Adds a trade to the wandering trader.\n     *\n     * @param trade  The trade for the wandering trader to offer.\n     * @param isRare If the trade should be added to the rare or common pool.\n     */\n    public void addWanderingTrade(VillagerTrades.ItemListing trade, boolean isRare) {\n        (isRare ? rareTrades : commonTrades).add(trade);\n    }\n\n    /**\n     * Adds a trade to the wandering traders common trades pool.\n     *\n     * @param trade the trade for the wandering trader to offer.\n     */\n    public void addCommonWanderingTrade(VillagerTrades.ItemListing trade) {\n        this.addWanderingTrade(trade, false);\n    }\n\n    /**\n     * Adds a trade to the wandering traders rare trades pool.\n     *\n     * @param trade the trade for the wandering trader to offer.\n     */\n    public void addRareWanderingTrade(VillagerTrades.ItemListing trade) {\n        this.addWanderingTrade(trade, true);\n    }\n\n    /**\n     * Gets a read-only view of the villager trades to register. Only contains trades added by the content provider.\n     *\n     * @return A read-only map of villager trades to register.\n     */\n    public Map<VillagerProfession, Multimap<Integer, VillagerTrades.ItemListing>> getVillagerTrades() {\n        return Collections.unmodifiableMap(this.villagerTrades);\n    }\n\n    /**\n     * Gets a read-only view of the rare wandering trader trades. Only contains trades added by the content provider.\n     *\n     * @return The rare wandering trader trades.\n     */\n    public List<VillagerTrades.ItemListing> getRareWanderingTrades() {\n        return Collections.unmodifiableList(this.rareTrades);\n    }\n\n    /**\n     * Gets a read-only view of the common wandering trader trades. Only contains trades added by the content provider.\n     *\n     * @return the common wandering trader trades.\n     */\n    public List<VillagerTrades.ItemListing> getCommonWanderingTrades() {\n        return Collections.unmodifiableList(this.commonTrades);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/impl/resources/ExtendedText.java",
    "content": "package net.darkhax.bookshelf.common.impl.resources;\n\nimport net.darkhax.bookshelf.common.api.ModEntry;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.DetectedVersion;\nimport net.minecraft.client.Minecraft;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\npublic class ExtendedText {\n\n    public static final Supplier<ExtendedText> INSTANCE = CachedSupplier.cache(ExtendedText::new);\n    private final Map<String, Supplier<String>> extendedEntries = new LinkedHashMap<>();\n\n    private ExtendedText() {\n\n        this.register(\"java.version\", () -> getProperty(\"java.version\"));\n        this.register(\"minecraft.version\", DetectedVersion.BUILT_IN::getName);\n        this.register(\"loader.name\", Services.PLATFORM::getName);\n        this.register(\"player.name\", () -> Minecraft.getInstance().getUser().getName());\n\n        for (ModEntry mod : Services.PLATFORM.getLoadedMods()) {\n            this.register(\"mods.\" + mod.modId() + \".name\", mod::name);\n            this.register(\"mods.\" + mod.modId() + \".desc\", mod::description);\n            this.register(\"mods.\" + mod.modId() + \".id\", mod::modId);\n            this.register(\"mods.\" + mod.modId() + \".version\", mod::version);\n        }\n    }\n\n    public boolean has(String key) {\n        return this.extendedEntries.containsKey(key);\n    }\n\n    public String get(String key) {\n        return this.extendedEntries.get(key).get();\n    }\n\n    private void register(String key, Supplier<String> value) {\n        extendedEntries.put(\"text.bookshelf.ext.\" + key, CachedSupplier.cache(value));\n    }\n\n    private static String getProperty(String propertyName) {\n        try {\n            return System.getProperty(propertyName);\n        }\n        catch (Exception e) {\n            Constants.LOG.debug(\"Unable to read property {}\", propertyName, e);\n            return \"unknown\";\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBannerBlockEntity.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.block;\n\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.level.block.entity.BannerBlockEntity;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(BannerBlockEntity.class)\npublic interface AccessorBannerBlockEntity {\n\n    @Accessor(\"name\")\n    void setName(Component name);\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBaseContainerBlockEntity.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.block;\n\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.level.block.entity.BaseContainerBlockEntity;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(BaseContainerBlockEntity.class)\npublic interface AccessorBaseContainerBlockEntity {\n\n    @Accessor(\"name\")\n    void bookshelf$name(Component name);\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBlockEntityRenderers.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.block;\n\nimport net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;\nimport net.minecraft.client.renderer.blockentity.BlockEntityRenderers;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(BlockEntityRenderers.class)\npublic interface AccessorBlockEntityRenderers {\n\n    @Invoker(\"register\")\n    @SuppressWarnings(\"rawtypes\")\n    static void bookshelf$register(BlockEntityType type, BlockEntityRendererProvider renderProvider) {\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorCropBlock.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.block;\n\nimport net.minecraft.world.level.ItemLike;\nimport net.minecraft.world.level.block.CropBlock;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(CropBlock.class)\npublic interface AccessorCropBlock {\n\n    @Invoker(\"getBaseSeedId\")\n    ItemLike bookshelf$getSeed();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorFontManager.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.client;\n\nimport net.minecraft.client.gui.font.FontManager;\nimport net.minecraft.client.gui.font.FontSet;\nimport net.minecraft.resources.ResourceLocation;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.Map;\n\n@Mixin(FontManager.class)\npublic interface AccessorFontManager {\n\n    @Accessor(\"fontSets\")\n    Map<ResourceLocation, FontSet> bookshelf$getFonts();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorItemBlockRenderTypes.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.client;\n\nimport net.minecraft.client.renderer.ItemBlockRenderTypes;\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.world.level.block.Block;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.Map;\n\n@Mixin(ItemBlockRenderTypes.class)\npublic interface AccessorItemBlockRenderTypes {\n\n    @Accessor(\"TYPE_BY_BLOCK\")\n    static Map<Block, RenderType> bookshelf$getBlockTypes() {\n        throw new IllegalStateException(\"Accessor code not reachable.\");\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorMinecraft.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.client;\n\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.font.FontManager;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(Minecraft.class)\npublic interface AccessorMinecraft {\n\n    @Accessor(\"fontManager\")\n    FontManager bookshelf$getFontManager();\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/gui/AccessorAbstractWidget.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.client.gui;\n\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(AbstractWidget.class)\npublic interface AccessorAbstractWidget {\n\n    @Invoker(\"renderScrollingString\")\n    static void bookshelf$renderScrollingString(GuiGraphics guiGraphics, Font font, Component text, int minX, int minY, int maxX, int maxY, int color) {\n        throw new IllegalStateException(\"Mixins failed to apply.\");\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/entity/AccessorEntity.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.entity;\n\nimport net.minecraft.network.chat.HoverEvent;\nimport net.minecraft.world.entity.Entity;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(Entity.class)\npublic interface AccessorEntity {\n\n    @Invoker(\"createHoverEvent\")\n    HoverEvent bookshelf$createHoverEvent();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/level/AccessorRecipeManager.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.level;\n\nimport com.google.common.collect.Multimap;\nimport net.minecraft.world.item.crafting.RecipeHolder;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport net.minecraft.world.item.crafting.RecipeType;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(RecipeManager.class)\npublic interface AccessorRecipeManager {\n\n    @Accessor(\"byType\")\n    Multimap<RecipeType<?>, RecipeHolder<?>> bookshelf$byTypeMap();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorCompositeEntryBase.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.world.level.storage.loot.entries.CompositeEntryBase;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.List;\n\n@Mixin(CompositeEntryBase.class)\npublic interface AccessorCompositeEntryBase {\n\n    @Accessor(\"children\")\n    List<LootPoolEntryContainer> bookshelf$children();\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorDynamicLoot.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.entries.DynamicLoot;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(DynamicLoot.class)\npublic interface AccessorDynamicLoot {\n\n    @Accessor(\"name\")\n    ResourceLocation bookshelf$name();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootItem.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.core.Holder;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.storage.loot.entries.LootItem;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunction;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemCondition;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\nimport java.util.List;\n\n@Mixin(LootItem.class)\npublic interface AccessorLootItem {\n\n    @Invoker(\"<init>\")\n    static LootItem bookshelf$create(Holder<Item> item, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions) {\n        return null;\n    }\n\n    @Accessor(\"item\")\n    Holder<Item> bookshelf$item();\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootPool.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunction;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemCondition;\nimport net.minecraft.world.level.storage.loot.providers.number.NumberProvider;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.List;\n\n@Mixin(LootPool.class)\npublic interface AccessorLootPool {\n\n    @Accessor(\"entries\")\n    List<LootPoolEntryContainer> bookshelf$entries();\n\n    @Accessor(\"entries\")\n    @Mutable\n    void bookshelf$setEntries(List<LootPoolEntryContainer> entries);\n\n    @Accessor(\"conditions\")\n    List<LootItemCondition> bookshelf$conditions();\n\n    @Accessor(\"functions\")\n    List<LootItemFunction> functions();\n\n    @Accessor(\"rolls\")\n    NumberProvider bookshelf$rolls();\n\n    @Accessor(\"bonusRolls\")\n    NumberProvider bookshelf$bonusRolls();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootPoolSingletonContainer.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.world.level.storage.loot.entries.LootPoolSingletonContainer;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(LootPoolSingletonContainer.class)\npublic interface AccessorLootPoolSingletonContainer {\n\n    @Accessor(\"weight\")\n    int bookshelf$weight();\n\n    @Accessor(\"quality\")\n    int bookshelf$quality();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootTable.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport net.minecraft.world.level.storage.loot.functions.LootItemFunction;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.List;\nimport java.util.Optional;\n\n@Mixin(LootTable.class)\npublic interface AccessorLootTable {\n\n    @Accessor(\"randomSequence\")\n    Optional<ResourceLocation> bookshelf$randomSequence();\n\n    @Accessor(\"pools\")\n    List<LootPool> bookshelf$pools();\n\n    @Accessor(\"functions\")\n    List<LootItemFunction> bookshelf$functions();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorNestedLootTable.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport com.mojang.datafixers.util.Either;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport net.minecraft.world.level.storage.loot.entries.NestedLootTable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(NestedLootTable.class)\npublic interface AccessorNestedLootTable {\n\n    @Accessor(\"contents\")\n    Either<ResourceKey<LootTable>, LootTable> bookshelf$contents();\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorTagEntry.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.loot;\n\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.storage.loot.entries.TagEntry;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(TagEntry.class)\npublic interface AccessorTagEntry {\n\n    @Accessor(\"tag\")\n    TagKey<Item> bookshelf$tag();\n\n    @Accessor(\"expand\")\n    boolean bookshelf$expand();\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/access/particles/AccessSimpleParticleType.java",
    "content": "package net.darkhax.bookshelf.common.mixin.access.particles;\n\nimport net.minecraft.core.particles.SimpleParticleType;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(SimpleParticleType.class)\npublic interface AccessSimpleParticleType {\n\n    @Invoker(\"<init>\")\n    static SimpleParticleType init(boolean overrideLimit) {\n        throw new IllegalStateException(\"That didn't mix well...\");\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/advancement/MixinPlayerAdvancements.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.advancement;\n\nimport net.darkhax.bookshelf.common.impl.data.criterion.trigger.AdvancementTrigger;\nimport net.minecraft.advancements.AdvancementHolder;\nimport net.minecraft.advancements.AdvancementProgress;\nimport net.minecraft.server.PlayerAdvancements;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(PlayerAdvancements.class)\npublic abstract class MixinPlayerAdvancements {\n\n    @Shadow\n    private ServerPlayer player;\n\n    @Shadow\n    public abstract AdvancementProgress getOrStartProgress(AdvancementHolder advHolder);\n\n    @Inject(method = \"award(Lnet/minecraft/advancements/AdvancementHolder;Ljava/lang/String;)Z\", at = @At(\"RETURN\"))\n    private void onAward(AdvancementHolder advancement, String criterion, CallbackInfoReturnable<Boolean> cir) {\n        if (this.getOrStartProgress(advancement).isDone()) {\n            AdvancementTrigger.TRIGGER.trigger(this.player, advancement);\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/block/MixinDecoratedPotPatterns.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.block;\n\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.block.entity.DecoratedPotPattern;\nimport net.minecraft.world.level.block.entity.DecoratedPotPatterns;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(DecoratedPotPatterns.class)\npublic class MixinDecoratedPotPatterns {\n\n    @Inject(method = \"getPatternFromItem\", at = @At(\"TAIL\"), cancellable = true)\n    private static void getResourceKey(Item item, CallbackInfoReturnable<ResourceKey<DecoratedPotPattern>> cbi) {\n        if (RegistrationContext.POT_PATTERN_ITEMS.containsKey(item)) {\n            cbi.setReturnValue(RegistrationContext.POT_PATTERN_ITEMS.get(item));\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/client/MixinClientPacketListener.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.client;\n\nimport net.darkhax.bookshelf.common.api.data.ISidedRecipeManager;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.multiplayer.ClientPacketListener;\nimport net.minecraft.client.multiplayer.CommonListenerCookie;\nimport net.minecraft.network.Connection;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(value = ClientPacketListener.class, priority = 1005)\npublic class MixinClientPacketListener {\n\n    @Shadow\n    @Final\n    private RecipeManager recipeManager;\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    public void onInit(Minecraft mc, Connection connection, CommonListenerCookie cookie, CallbackInfo ci) {\n        if (this.recipeManager instanceof ISidedRecipeManager sided) {\n            sided.bookshelf$setLogicalClient();\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/entity/MixinLightningBolt.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.entity;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport net.darkhax.bookshelf.common.api.block.IBlockHooks;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.world.entity.LightningBolt;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.Blocks;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(LightningBolt.class)\npublic class MixinLightningBolt {\n\n    @Inject(method = \"powerLightningRod\", at = @At(\"RETURN\"))\n    private void onLightningStrike(CallbackInfo ci, @Local BlockPos strikePos, @Local BlockState strikeState) {\n        final LightningBolt self = (LightningBolt) (Object) this;\n        final Block strikeBlock = strikeState.getBlock();\n        Direction[] redirections = strikeBlock == Blocks.LIGHTNING_ROD ? IBlockHooks.LIGHTNING_REDIRECTION_FACES : IBlockHooks.NO_LIGHTNING_REDIRECTION_FACES;\n        if (strikeBlock instanceof IBlockHooks extended) {\n            extended.onLightningStrike(strikeState, self.level(), strikePos, self);\n            redirections = extended.redirectLightningStrike(strikeState, self.level(), strikePos);\n        }\n        for (Direction direction : redirections) {\n            final BlockPos indirectPos = strikePos.relative(direction);\n            final BlockState indirectState = self.level().getBlockState(indirectPos);\n            if (indirectState.getBlock() instanceof IBlockHooks extended) {\n                extended.onLightningStrikeIndirect(indirectState, self.level(), indirectPos, self, strikePos);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/entity/MixinLivingEntity.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.entity;\n\nimport net.darkhax.bookshelf.common.api.data.BookshelfTags;\nimport net.minecraft.world.damagesource.DamageSource;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.LivingEntity;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(LivingEntity.class)\npublic abstract class MixinLivingEntity extends Entity {\n\n    @Shadow\n    protected int lastHurtByPlayerTime;\n\n    @Shadow\n    private int lastHurtByMobTimestamp;\n\n    /**\n     * This patch allows mobs killed by Bookshelf's fake player damage to drop EXP and player specific loot. Bookshelf's\n     * fake player damage is not connected to a specific entity instance so the timers responsible for these checks are\n     * not updated otherwise.\n     */\n    @Inject(method = \"hurt\", at = @At(\"HEAD\"))\n    private void updateFakePlayerDamageTimes(DamageSource source, float amount, CallbackInfoReturnable<Boolean> callback) {\n        if (!this.level().isClientSide && !this.isInvulnerableTo(source) && source.is(BookshelfTags.FAKE_PLAYER_DAMAGE)) {\n            this.lastHurtByPlayerTime = this.tickCount;\n            this.lastHurtByMobTimestamp = this.tickCount;\n        }\n    }\n\n    private MixinLivingEntity() {\n        super(null, null);\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/item/MixinCreativeModeTab.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.item;\n\nimport net.darkhax.bookshelf.common.api.item.IItemHooks;\nimport net.darkhax.bookshelf.common.api.util.DataHelper;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\n@Mixin(CreativeModeTab.class)\npublic class MixinCreativeModeTab {\n\n    @Shadow\n    private Collection<ItemStack> displayItems;\n\n    @Shadow\n    private Set<ItemStack> displayItemsSearchTab;\n\n    @Unique\n    private static final Map<ResourceLocation, TagKey<Item>> TAG_CACHE = new HashMap<>();\n\n    @Unique\n    private static final ResourceLocation OP_ITEMS_ID = ResourceLocation.fromNamespaceAndPath(\"minecraft\", \"op_blocks\");\n\n    @Inject(method = \"buildContents(Lnet/minecraft/world/item/CreativeModeTab$ItemDisplayParameters;)V\", at = @At(\"TAIL\"))\n    private void buildContents(CreativeModeTab.ItemDisplayParameters parameters, CallbackInfo cbi) {\n        final CreativeModeTab self = (CreativeModeTab) (Object) this;\n        final ResourceLocation id = BuiltInRegistries.CREATIVE_MODE_TAB.getKey(self);\n        if (id != null && (!self.isAlignedRight() || id.equals(OP_ITEMS_ID)) && (!id.equals(OP_ITEMS_ID) || parameters.hasPermissions())) {\n            final TagKey<Item> tabTag = TAG_CACHE.computeIfAbsent(id, key -> TagKey.create(Registries.ITEM, Constants.id(\"creative_tab/\" + key.getNamespace() + \"/\" + key.getPath())));\n            for (Holder<Item> tagEntry : DataHelper.getTagOrEmpty(parameters.holders(), Registries.ITEM, tabTag)) {\n                try {\n                    final Item item = tagEntry.value();\n                    if (item instanceof IItemHooks hooks) {\n                        hooks.addCreativeTabForms(self, stack -> {\n                            displayItems.add(stack);\n                            displayItemsSearchTab.add(stack);\n                        });\n                    }\n                    else {\n                        final ItemStack stack = new ItemStack(item);\n                        displayItems.add(stack);\n                        displayItemsSearchTab.add(stack);\n                    }\n                }\n                catch (Exception e) {\n                    Constants.LOG.error(\"Unable to add tag entries to creative tab!\", e);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/level/MixinRecipeManager.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.level;\n\nimport com.google.gson.JsonElement;\nimport net.darkhax.bookshelf.common.api.data.ISidedRecipeManager;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport net.minecraft.util.profiling.ProfilerFiller;\nimport net.minecraft.world.item.crafting.RecipeHolder;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.Map;\n\n@Mixin(RecipeManager.class)\npublic class MixinRecipeManager implements ISidedRecipeManager {\n\n    @Unique\n    private boolean bookshelf$isClient = false;\n\n    @Unique\n    private boolean bookshelf$isServer = false;\n\n    @Inject(method = \"apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V\", at = @At(\"RETURN\"))\n    private void onReload(Map<ResourceLocation, JsonElement> object, ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfo ci) {\n        if (this.bookshelf$isServer) {\n            Constants.SERVER_REVISION++;\n        }\n    }\n\n    @Inject(method = \"replaceRecipes\", at = @At(\"RETURN\"))\n    private void onRecipesUpdated(Iterable<RecipeHolder<?>> recipes, CallbackInfo ci) {\n        if (this.bookshelf$isClient) {\n            Constants.CLIENT_REVISION++;\n        }\n    }\n\n    @Override\n    public void bookshelf$setLogicalClient() {\n        this.bookshelf$isClient = true;\n        this.bookshelf$isServer = false;\n    }\n\n    @Override\n    public void bookshelf$setLogicalServer() {\n        this.bookshelf$isServer = true;\n        this.bookshelf$isClient = false;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/level/MixinWalkNodeEvaluator.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.level;\n\nimport net.darkhax.bookshelf.common.api.block.IBlockHooks;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.level.BlockGetter;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.pathfinder.PathType;\nimport net.minecraft.world.level.pathfinder.WalkNodeEvaluator;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\nimport org.spongepowered.asm.mixin.injection.callback.LocalCapture;\n\n@Mixin(WalkNodeEvaluator.class)\npublic class MixinWalkNodeEvaluator {\n\n    /**\n     * This patch allows modded blocks to control their own pathfinding type. This is done by implementing\n     * {@link IBlockHooks#getPathfindingType(BlockState, BlockGetter, BlockPos)}.\n     */\n    @Inject(method = \"getPathTypeFromState(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/pathfinder/PathType;\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;\"), locals = LocalCapture.CAPTURE_FAILSOFT, cancellable = true)\n    private static void getBlockPathTypeRaw(BlockGetter level, BlockPos pos, CallbackInfoReturnable<PathType> cbi, BlockState state) {\n        if (state.getBlock() instanceof IBlockHooks hooks) {\n            final PathType customType = hooks.getPathfindingType(state, level, pos);\n            if (customType != null) {\n                cbi.setReturnValue(customType);\n            }\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/locale/MixinClientLanguage.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.locale;\n\nimport net.darkhax.bookshelf.common.impl.resources.ExtendedText;\nimport net.minecraft.client.resources.language.ClientLanguage;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(ClientLanguage.class)\npublic class MixinClientLanguage {\n\n    @Inject(method = \"getOrDefault\", at = @At(\"HEAD\"), cancellable = true)\n    public void getOrDefault(String key, String fallback, CallbackInfoReturnable<String> cbi) {\n        if (ExtendedText.INSTANCE.get().has(key)) {\n            cbi.setReturnValue(ExtendedText.INSTANCE.get().get(key));\n        }\n    }\n\n    @Inject(method = \"has\", at = @At(\"HEAD\"), cancellable = true)\n    public void has(String key, CallbackInfoReturnable<Boolean> cbi) {\n        if (ExtendedText.INSTANCE.get().has(key)) {\n            cbi.setReturnValue(true);\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootDataType.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.loot;\n\nimport com.google.gson.JsonObject;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.DynamicOps;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.impl.data.loot.modifiers.LootModificationHandler;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.storage.loot.LootDataType;\nimport net.minecraft.world.level.storage.loot.LootTable;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\nimport org.spongepowered.asm.mixin.injection.callback.LocalCapture;\n\nimport java.util.Optional;\n\n@Mixin(LootDataType.class)\npublic class MixinLootDataType {\n\n    @Inject(method = \"deserialize(Lnet/minecraft/resources/ResourceLocation;Lcom/mojang/serialization/DynamicOps;Ljava/lang/Object;)Ljava/util/Optional;\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/serialization/DataResult;error()Ljava/util/Optional;\"), locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true)\n    private void onDeserialize(ResourceLocation id, DynamicOps<?> ops, Object value, CallbackInfoReturnable<Optional<?>> cir, DataResult<?> result) {\n        // Allow bookshelf load conditions to be used on loot tables.\n        if (value instanceof JsonObject obj && !LoadConditions.canLoad(obj)) {\n            cir.setReturnValue(Optional.empty());\n            return;\n        }\n        // These conditions have been split up because IDEA thinks it will always be false.\n        // This is not the case, and is related to mixin shenanigans.\n        if ((Object) this == LootDataType.TABLE) {\n            if (value instanceof JsonObject && result.error().isEmpty()) {\n                final Object rst = result.result().orElse(null);\n                LootTable table = bookshelf$getLootTable(rst);\n                if (table != null) {\n                    LootModificationHandler.HANDLER.get().processLootTable(id, table);\n                }\n            }\n        }\n    }\n\n    @Nullable\n    @Unique\n    private static LootTable bookshelf$getLootTable(Object rst) {\n        // Under normal circumstances rst is always a LootTable but NeoForge has\n        // patched the code to use Optional<LootTable> instead so we need to\n        // check and resolve those as well.\n        LootTable table = null;\n        if (rst instanceof LootTable lt) {\n            table = lt;\n        }\n        else if (rst instanceof Optional<?> optionalObj && optionalObj.orElse(null) instanceof LootTable lt) {\n            table = lt;\n        }\n        return table;\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootItemKilledByPlayerCondition.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.loot;\n\nimport net.darkhax.bookshelf.common.api.data.BookshelfTags;\nimport net.minecraft.world.damagesource.DamageSource;\nimport net.minecraft.world.level.storage.loot.LootContext;\nimport net.minecraft.world.level.storage.loot.parameters.LootContextParams;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemKilledByPlayerCondition;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(LootItemKilledByPlayerCondition.class)\npublic class MixinLootItemKilledByPlayerCondition {\n\n    /**\n     * This patch allows mobs that were killed with Bookshelfs fake player damage to satisfy the\n     * minecraft:killed_by_player loot condition.\n     */\n    @Inject(method = \"test(Lnet/minecraft/world/level/storage/loot/LootContext;)Z\", at = @At(\"HEAD\"), cancellable = true)\n    public void test(LootContext context, CallbackInfoReturnable<Boolean> callback) {\n        if (context != null && context.hasParam(LootContextParams.DAMAGE_SOURCE)) {\n            final DamageSource source = context.getParam(LootContextParams.DAMAGE_SOURCE);\n            if (source.is(BookshelfTags.FAKE_PLAYER_DAMAGE)) {\n                callback.setReturnValue(true);\n            }\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootPool.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.loot;\n\nimport com.mojang.serialization.Codec;\nimport net.darkhax.bookshelf.common.impl.data.loot.modifiers.FingerprintCodec;\nimport net.darkhax.bookshelf.common.impl.data.loot.modifiers.ILootPoolHooks;\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(LootPool.class)\npublic class MixinLootPool implements ILootPoolHooks {\n\n    @Shadow\n    @Final\n    @Mutable\n    public static Codec<LootPool> CODEC;\n\n    @Unique\n    private Integer bookshelf$fingerprint = null;\n\n    @Inject(method = \"<clinit>\", at = @At(\"RETURN\"))\n    private static void onClassInit(CallbackInfo ci) {\n        CODEC = new FingerprintCodec<>(CODEC);\n    }\n\n    @Override\n    public void bookshelf$setHash(int fingerprint) {\n        this.bookshelf$fingerprint = fingerprint;\n    }\n\n    @Override\n    public Integer bookshelf$getHash() {\n        return this.bookshelf$fingerprint;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/packs/MixinSimpleJsonResourceReloadListener.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.packs;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;\nimport net.minecraft.util.profiling.ProfilerFiller;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.Map;\n\n@Mixin(SimpleJsonResourceReloadListener.class)\npublic class MixinSimpleJsonResourceReloadListener {\n\n    /**\n     * This patch introduces load conditions for all JSON based resource loaders. These conditions are independent of\n     * the loader platform allowing them to be used in loader agnostic sourcesets.\n     */\n    @Inject(method = \"prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Ljava/util/Map;\", at = @At(\"RETURN\"))\n    private void prepare(ResourceManager manager, ProfilerFiller profiler, CallbackInfoReturnable<Map<ResourceLocation, JsonElement>> cbi) {\n        cbi.getReturnValue().entrySet().removeIf(entry -> entry.getValue() instanceof JsonObject obj && !LoadConditions.canLoad(obj));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/potions/MixinPotionBrewing.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.potions;\n\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.minecraft.world.item.alchemy.PotionBrewing;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(PotionBrewing.class)\npublic class MixinPotionBrewing {\n\n    @Inject(method = \"addVanillaMixes\", at = @At(\"RETURN\"))\n    private static void onBootstrap(PotionBrewing.Builder builder, CallbackInfo ci) {\n        Services.CONTENT.get().forEach(provider -> provider.defineBrews(builder));\n    }\n}"
  },
  {
    "path": "common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/server/MixinReloadableServerResources.java",
    "content": "package net.darkhax.bookshelf.common.mixin.patch.server;\n\nimport net.darkhax.bookshelf.common.api.data.ISidedRecipeManager;\nimport net.minecraft.commands.Commands;\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.server.ReloadableServerResources;\nimport net.minecraft.world.flag.FeatureFlagSet;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(ReloadableServerResources.class)\npublic class MixinReloadableServerResources {\n\n    @Shadow @Final private RecipeManager recipes;\n\n    @Inject(method = \"<init>\", at = @At(\"RETURN\"))\n    private void onInit(RegistryAccess.Frozen registry, FeatureFlagSet features, Commands.CommandSelection commands, int functionLevel, CallbackInfo ci) {\n        if (this.recipes instanceof ISidedRecipeManager sided) {\n            sided.bookshelf$setLogicalServer();\n        }\n    }\n}"
  },
  {
    "path": "common/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.registry.ContentProvider",
    "content": "net.darkhax.bookshelf.common.impl.BookshelfContent"
  },
  {
    "path": "common/src/main/resources/assets/bookshelf/lang/en_us.json",
    "content": "{\n  \"__formatting\": \"\",\n  \"format.bookshelf.right\": \"%s: %s\",\n  \"format.bookshelf.center\": \"%s : %s\",\n  \"format.bookshelf.left\": \"%s :%s\",\n  \"format.bookshelf.spaced\": \"%s %s\",\n  \"format.bookshelf.none\": \"%s%s\",\n  \"format.bookshelf.unit_rate\": \"%s/%s\",\n\n  \"__commands\": \"Command Text\",\n  \"commands.bookshelf.hand.error.not_air\": \"Item must not be empty or air!\",\n  \"commands.bookshelf.hand.error.internal\": \"Error encountered while formatting text. Check logs for more info.\",\n  \"commands.bookshelf.font.unsupported_block\": \"Could not apply font to '%s'. This type of block is not supported.\",\n  \"commands.bookshelf.font.bad_sender\": \"The font rename command\",\n  \"commands.bookshelf.debug.no_info\": \"No debug information available for this command.\",\n  \"commands.bookshelf.debug.yes_info\": \"Debug information has been logged to your game log. Click to copy output.\",\n  \"commands.bookshelf.debug.too_long\": \"Debug output is too big for chat. Please check your game log instead.\",\n  \"commands.bookshelf.structure.error.no_structures\": \"No structures could be found.\",\n  \"commands.bookshelf.structure.found\": \"Found %s structure(s)!\",\n\n  \"__time_units\": \"Entries for various time units that may be displayed in game.\",\n  \"units.bookshelf.tick\": \"Tick\",\n  \"units.bookshelf.tick.plural\": \"Ticks\",\n  \"units.bookshelf.tick.abbreviated\": \"t\",\n  \"units.bookshelf.nanosecond\": \"Nanosecond\",\n  \"units.bookshelf.nanosecond.plural\": \"Nanoseconds\",\n  \"units.bookshelf.nanosecond.abbreviated\": \"ns\",\n  \"units.bookshelf.millisecond\": \"Millisecond\",\n  \"units.bookshelf.millisecond.plural\": \"Milliseconds\",\n  \"units.bookshelf.millisecond.abbreviated\": \"ms\",\n  \"units.bookshelf.second\": \"Second\",\n  \"units.bookshelf.second.plural\": \"Seconds\",\n  \"units.bookshelf.second.abbreviated\": \"s\",\n  \"units.bookshelf.minute\": \"Minute\",\n  \"units.bookshelf.minute.plural\": \"Minutes\",\n  \"units.bookshelf.minute.abbreviated\": \"m\",\n  \"units.bookshelf.hour\": \"Hour\",\n  \"units.bookshelf.hour.plural\": \"Hours\",\n  \"units.bookshelf.hour.abbreviated\": \"h\",\n  \"units.bookshelf.day\": \"Day\",\n  \"units.bookshelf.day.plural\": \"Days\",\n  \"units.bookshelf.day.abbreviated\": \"d\",\n  \"units.bookshelf.week\": \"Week\",\n  \"units.bookshelf.week.plural\": \"Weeks\",\n  \"units.bookshelf.week.abbreviated\": \"wk\",\n  \"units.bookshelf.month\": \"Month\",\n  \"units.bookshelf.month.plural\": \"Months\",\n  \"units.bookshelf.month.abbreviated\": \"mo\",\n  \"units.bookshelf.year\": \"Year\",\n  \"units.bookshelf.year.plural\": \"Years\",\n  \"units.bookshelf.year.abbreviated\": \"yr\",\n\n  \"__months\": \"Names of the months\",\n  \"month.bookshelf.january\": \"January\",\n  \"month.bookshelf.february\": \"February\",\n  \"month.bookshelf.march\": \"March\",\n  \"month.bookshelf.april\": \"April\",\n  \"month.bookshelf.may\": \"May\",\n  \"month.bookshelf.june\": \"June\",\n  \"month.bookshelf.july\": \"July\",\n  \"month.bookshelf.august\": \"August\",\n  \"month.bookshelf.september\": \"September\",\n  \"month.bookshelf.october\": \"October\",\n  \"month.bookshelf.november\": \"November\",\n  \"month.bookshelf.december\": \"December\",\n\n  \"__days\": \"Names of the days\",\n  \"day.bookshelf.sunday\": \"Sunday\",\n  \"day.bookshelf.monday\": \"Monday\",\n  \"day.bookshelf.tuesday\": \"Tuesday\",\n  \"day.bookshelf.wednesday\": \"Wednesday\",\n  \"day.bookshelf.thursday\": \"Thursday\",\n  \"day.bookshelf.friday\": \"Friday\",\n  \"day.bookshelf.saturday\": \"Saturday\",\n\n  \"__moon_phases\": \"The names of different moon phases that appear in game.\",\n  \"moon.phase.full\": \"Full Moon\",\n  \"moon.phase.waxing.gibbous\": \"Waxing Gibbous\",\n  \"moon.phase.first.quarter\": \"First Quarter\",\n  \"moon.phase.waxing.crescent\": \"Waxing Crescent\",\n  \"moon.phase.new\": \"New Moon\",\n  \"moon.phase.waning.crescent\": \"Waning Crescent\",\n  \"moon.phase.last.quarter\": \"Last Quarter\",\n  \"moon.phase.waning.gibbous\": \"Waning Gibbous\",\n\n  \"__fonts\": \"Unofficial language entries for the vanilla text fonts.\",\n  \"font.minecraft.default\": \"Default\",\n  \"font.minecraft.default.desc\": \"The standard font in Minecraft.\",\n  \"font.minecraft.default.preview\": \"The quick brown fox jumps over the lazy dog.\",\n  \"font.minecraft.alt\": \"Standard Galactic Alphabet\",\n  \"font.minecraft.alt.desc\": \"A rune based font associated with enchanting and magic.\",\n  \"font.minecraft.alt.preview\": \"Majik fox cub solved the waspy dragons quiz\",\n  \"font.minecraft.illageralt\": \"Illager Alphabet\",\n  \"font.minecraft.illageralt.desc\": \"A mysterious font used by the illagers.\",\n  \"font.minecraft.illageralt.preview\": \"Grumpy wizards make a toxic brew for the jovial queen.\",\n  \"font.minecraft.uniform\": \"Uniform\",\n  \"font.minecraft.uniform.desc\": \"A plain font that is not stylized.\",\n  \"font.minecraft.uniform.preview\": \"The quick brown fox jumps over the lazy dog.\",\n\n  \"__Tags\": \"Tag Names for Recipe Viewers\",\n  \"tag.item.bookshelf.creative_tab.minecraft.colored_blocks\": \"Colored Blocks Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.food_and_drinks\": \"Food and Drinks Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.spawn_eggs\": \"Spawn Eggs Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.redstone_blocks\": \"Redstone Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.op_blocks\": \"OP/Admin Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.combat\": \"Combat Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.building_blocks\": \"Building Blocks Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities\": \"Tools & Utilities Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.natural_blocks\": \"Natural Blocks Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.functional_blocks\": \"Functional Blocks Tab\",\n  \"tag.item.bookshelf.creative_tab.minecraft.ingredients\": \"Ingredients Tab\",\n\n  \"tooltips.bookshelf.loot.unknown\": \"Unknown Drop\",\n  \"tooltips.bookshelf.loot.unknown.desc\": \"Some drops can not be displayed right now.\",\n  \"tooltips.bookshelf.loot.empty\": \"Empty Drop\",\n  \"tooltips.bookshelf.loot.empty.desc\": \"It's possible for nothing to drop!\",\n  \"tooltips.bookshelf.loot.dynamic\": \"Dynamic Drop\",\n  \"tooltips.bookshelf.loot.dynamic.desc\": \"Some drops depend on the context.\",\n\n  \"gui.jei.category.loot.name\": \"Loot Table\",\n\n  \"table.minecraft.archaeology.desert_pyramid.name\": \"Desert Pyramid\",\n  \"table.minecraft.archaeology.desert_well.name\": \"Desert Well\",\n  \"table.minecraft.archaeology.ocean_ruin_cold.name\": \"Ocean Ruin - Cold\",\n  \"table.minecraft.archaeology.ocean_ruin_warm.name\": \"Ocean Ruin - Warm\",\n  \"table.minecraft.archaeology.trail_ruins_common.name\": \"Trail Ruins - Common\",\n  \"table.minecraft.archaeology.trail_ruins_rare.name\": \"Trail Ruins - Rare\",\n  \"table.minecraft.chests.abandoned_mineshaft.name\": \"Abandoned Mineshaft\",\n  \"table.minecraft.chests.ancient_city.name\": \"Ancient City\",\n  \"table.minecraft.chests.ancient_city_ice_box.name\": \"Ancient City Ice Box\",\n  \"table.minecraft.chests.bastion_bridge.name\": \"Bastion - Bridge\",\n  \"table.minecraft.chests.bastion_hoglin_stable.name\": \"Bastion - Hoglin Stable\",\n  \"table.minecraft.chests.bastion_other.name\": \"Bastion - Other\",\n  \"table.minecraft.chests.bastion_treasure.name\": \"Bastion - Treasure\",\n  \"table.minecraft.chests.buried_treasure.name\": \"Buried Treasure\",\n  \"table.minecraft.chests.desert_pyramid.name\": \"Desert Pyramid\",\n  \"table.minecraft.chests.end_city_treasure.name\": \"End City Treasure\",\n  \"table.minecraft.chests.igloo_chest.name\": \"Igloo Chest\",\n  \"table.minecraft.chests.jungle_temple.name\": \"Jungle Temple - Chest\",\n  \"table.minecraft.chests.jungle_temple_dispenser.name\": \"Jungle Temple - Dispenser\",\n  \"table.minecraft.chests.nether_bridge.name\": \"Nether Bridge\",\n  \"table.minecraft.chests.pillager_outpost.name\": \"Pillager Outpost\",\n  \"table.minecraft.chests.ruined_portal.name\": \"Ruined Portal\",\n  \"table.minecraft.chests.shipwreck_map.name\": \"Shipwreck Map\",\n  \"table.minecraft.chests.shipwreck_supply.name\": \"Shipwreck - Supply\",\n  \"table.minecraft.chests.shipwreck_treasure.name\": \"Shipwreck - Treasure\",\n  \"table.minecraft.chests.simple_dungeon.name\": \"Dungeon\",\n  \"table.minecraft.chests.spawn_bonus_chest.name\": \"Spawn Bonus Chest\",\n  \"table.minecraft.chests.stronghold_corridor.name\": \"Stronghold - Corridor\",\n  \"table.minecraft.chests.stronghold_crossing.name\": \"Stronghold - Crossing\",\n  \"table.minecraft.chests.stronghold_library.name\": \"Stronghold - Library\",\n  \"table.minecraft.chests.trial_chambers.corridor.name\": \"Trial Chamber - Corridor\",\n  \"table.minecraft.chests.trial_chambers.entrance.name\": \"Trial Chamber - Entrance\",\n  \"table.minecraft.chests.trial_chambers.intersection.name\": \"Trial Chamber - Intersection\",\n  \"table.minecraft.chests.trial_chambers.intersection_barrel.name\": \"Trial Chamber - Intersection Barrel\",\n  \"table.minecraft.chests.trial_chambers.reward.name\": \"Trial Chamber - Reward\",\n  \"table.minecraft.chests.trial_chambers.reward_common.name\": \"Trial Chamber - Common Reward\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous.name\": \"Trial Chamber - Ominous Reward\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_common.name\": \"Trial Chamber - Ominous Reward - Common\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_rare.name\": \"Trial Chamber - Ominous Reward - Rare\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_unique.name\": \"Trial Chamber - Ominous Reward - Unique\",\n  \"table.minecraft.chests.trial_chambers.reward_rare.name\": \"Trial Chamber - Rare Reward\",\n  \"table.minecraft.chests.trial_chambers.reward_unique.name\": \"Trial Chamber - Unique Reward\",\n  \"table.minecraft.chests.trial_chambers.supply.name\": \"Trial Chamber - Supply\",\n  \"table.minecraft.chests.underwater_ruin_big.name\": \"Underwater Ruin - Big\",\n  \"table.minecraft.chests.underwater_ruin_small.name\": \"Underwater Ruin - Small\",\n  \"table.minecraft.chests.village.village_armorer.name\": \"Village - Armorer\",\n  \"table.minecraft.chests.village.village_butcher.name\": \"Village - Butcher\",\n  \"table.minecraft.chests.village.village_cartographer.name\": \"Village - Cartographer\",\n  \"table.minecraft.chests.village.village_desert_house.name\": \"Village - Desert House\",\n  \"table.minecraft.chests.village.village_fisher.name\": \"Village - Fisher\",\n  \"table.minecraft.chests.village.village_fletcher.name\": \"Village - Fletcher\",\n  \"table.minecraft.chests.village.village_mason.name\": \"Village - Mason\",\n  \"table.minecraft.chests.village.village_plains_house.name\": \"Village - Plains House\",\n  \"table.minecraft.chests.village.village_savanna_house.name\": \"Village - Savanna House\",\n  \"table.minecraft.chests.village.village_shepherd.name\": \"Village - Shepherd\",\n  \"table.minecraft.chests.village.village_snowy_house.name\": \"Village - Snowy House\",\n  \"table.minecraft.chests.village.village_taiga_house.name\": \"Village - Taiga House\",\n  \"table.minecraft.chests.village.village_tannery.name\": \"Village - Tannery\",\n  \"table.minecraft.chests.village.village_temple.name\": \"Village - Temple\",\n  \"table.minecraft.chests.village.village_toolsmith.name\": \"Village - Toolsmith\",\n  \"table.minecraft.chests.village.village_weaponsmith.name\": \"Village - Weaponsmith\",\n  \"table.minecraft.chests.woodland_mansion.name\": \"Woodland Mansion\",\n  \"table.minecraft.dispensers.trial_chambers.chamber.name\": \"Trial Chamber - Chamber\",\n  \"table.minecraft.dispensers.trial_chambers.corridor.name\": \"Trial Chamber - Corridor\",\n  \"table.minecraft.dispensers.trial_chambers.water.name\": \"Trial Chamber - Water\",\n  \"table.minecraft.empty.name\": \"Empty\",\n  \"table.minecraft.equipment.trial_chamber.name\": \"Trial Chamber\",\n  \"table.minecraft.equipment.trial_chamber_melee.name\": \"Trial Chamber - Melee\",\n  \"table.minecraft.equipment.trial_chamber_ranged.name\": \"Trial Chamber - Ranged\",\n  \"table.minecraft.gameplay.cat_morning_gift.name\": \"Cat - Morning Gift\",\n  \"table.minecraft.gameplay.fishing.fish.name\": \"Fishing - Fish\",\n  \"table.minecraft.gameplay.fishing.junk.name\": \"Fishing - Junk\",\n  \"table.minecraft.gameplay.fishing.name\": \"Fishing\",\n  \"table.minecraft.gameplay.fishing.treasure.name\": \"Fishing - Treasure\",\n  \"table.minecraft.gameplay.hero_of_the_village.armorer_gift.name\": \"Hero Of The Village - Armorer\",\n  \"table.minecraft.gameplay.hero_of_the_village.butcher_gift.name\": \"Hero Of The Village - Butcher\",\n  \"table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name\": \"Hero Of The Village - Cartographer\",\n  \"table.minecraft.gameplay.hero_of_the_village.cleric_gift.name\": \"Hero Of The Village - Cleric\",\n  \"table.minecraft.gameplay.hero_of_the_village.farmer_gift.name\": \"Hero Of The Village - Farmer\",\n  \"table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name\": \"Hero Of The Village - Fisherman\",\n  \"table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name\": \"Hero Of The Village - Fletcher\",\n  \"table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name\": \"Hero Of The Village - Leatherworker\",\n  \"table.minecraft.gameplay.hero_of_the_village.librarian_gift.name\": \"Hero Of The Village - Librarian\",\n  \"table.minecraft.gameplay.hero_of_the_village.mason_gift.name\": \"Hero Of The Village - Mason\",\n  \"table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name\": \"Hero Of The Village - Shepherd\",\n  \"table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name\": \"Hero Of The Village - Toolsmith\",\n  \"table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name\": \"Hero Of The Village - Weaponsmith\",\n  \"table.minecraft.gameplay.panda_sneeze.name\": \"Panda Sneeze\",\n  \"table.minecraft.gameplay.piglin_bartering.name\": \"Piglin Bartering\",\n  \"table.minecraft.gameplay.sniffer_digging.name\": \"Sniffer Digging\",\n  \"table.minecraft.pots.trial_chambers.corridor.name\": \"Trial Chamber - Corridor Pot\",\n  \"table.minecraft.shearing.bogged.name\": \"Bogged - Shearing\",\n  \"table.minecraft.spawners.ominous.trial_chamber.consumables.name\": \"Trial Chamber - Ominous Consumables\",\n  \"table.minecraft.spawners.ominous.trial_chamber.key.name\": \"Trial Chamber - Ominous Key\",\n  \"table.minecraft.spawners.trial_chamber.consumables.name\": \"Trial Chamber - Consumables\",\n  \"table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name\": \"Trial Chamber - Ominous Drops\",\n  \"table.minecraft.spawners.trial_chamber.key.name\": \"Trial Chamber - Key\",\n  \"table.builtin.block_drops\": \"Block - %s\"\n}"
  },
  {
    "path": "common/src/main/resources/assets/bookshelf/lang/es_ar.json",
    "content": "{\n  \"__formatting\": \"\",\n  \"format.bookshelf.right\": \"%s: %s\",\n  \"format.bookshelf.center\": \"%s : %s\",\n  \"format.bookshelf.left\": \"%s :%s\",\n  \"format.bookshelf.spaced\": \"%s %s\",\n  \"format.bookshelf.none\": \"%s%s\",\n  \"format.bookshelf.unit_rate\": \"%s/%s\",\n\n  \"__commands\": \"Texto de Comandos\",\n  \"commands.bookshelf.hand.error.not_air\": \"¡El ítem no debe estar vacío o ser aire!\",\n  \"commands.bookshelf.hand.error.internal\": \"Error encontrado al formatear el texto. Revisá los registros para más información.\",\n  \"commands.bookshelf.font.unsupported_block\": \"No se pudo aplicar la fuente a '%s'. Este tipo de bloque no es compatible.\",\n  \"commands.bookshelf.font.bad_sender\": \"El comando para renombrar la fuente\",\n  \"commands.bookshelf.debug.no_info\": \"No hay información de depuración disponible para este comando.\",\n  \"commands.bookshelf.debug.yes_info\": \"La información de depuración se registró en tu archivo de juego. Hacé clic para copiar la salida.\",\n  \"commands.bookshelf.debug.too_long\": \"La salida de depuración es demasiado grande para el chat. Por favor, revisá tu archivo de juego en su lugar.\",\n  \"commands.bookshelf.structure.error.no_structures\": \"No se encontraron estructuras.\",\n  \"commands.bookshelf.structure.found\": \"¡Se encontraron %s estructura(s)!\",\n\n  \"__time_units\": \"Entradas para varias unidades de tiempo que pueden mostrarse en el juego.\",\n  \"units.bookshelf.tick\": \"Tick\",\n  \"units.bookshelf.tick.plural\": \"Ticks\",\n  \"units.bookshelf.tick.abbreviated\": \"t\",\n  \"units.bookshelf.nanosecond\": \"Nanosegundo\",\n  \"units.bookshelf.nanosecond.plural\": \"Nanosegundos\",\n  \"units.bookshelf.nanosecond.abbreviated\": \"ns\",\n  \"units.bookshelf.millisecond\": \"Milisegundo\",\n  \"units.bookshelf.millisecond.plural\": \"Milisegundos\",\n  \"units.bookshelf.millisecond.abbreviated\": \"ms\",\n  \"units.bookshelf.second\": \"Segundo\",\n  \"units.bookshelf.second.plural\": \"Segundos\",\n  \"units.bookshelf.second.abbreviated\": \"s\",\n  \"units.bookshelf.minute\": \"Minuto\",\n  \"units.bookshelf.minute.plural\": \"Minutos\",\n  \"units.bookshelf.minute.abbreviated\": \"m\",\n  \"units.bookshelf.hour\": \"Hora\",\n  \"units.bookshelf.hour.plural\": \"Horas\",\n  \"units.bookshelf.hour.abbreviated\": \"h\",\n  \"units.bookshelf.day\": \"Día\",\n  \"units.bookshelf.day.plural\": \"Días\",\n  \"units.bookshelf.day.abbreviated\": \"d\",\n  \"units.bookshelf.week\": \"Semana\",\n  \"units.bookshelf.week.plural\": \"Semanas\",\n  \"units.bookshelf.week.abbreviated\": \"sem\",\n  \"units.bookshelf.month\": \"Mes\",\n  \"units.bookshelf.month.plural\": \"Meses\",\n  \"units.bookshelf.month.abbreviated\": \"mes\",\n  \"units.bookshelf.year\": \"Año\",\n  \"units.bookshelf.year.plural\": \"Años\",\n  \"units.bookshelf.year.abbreviated\": \"año\",\n\n  \"__months\": \"Nombres de los meses\",\n  \"month.bookshelf.january\": \"Enero\",\n  \"month.bookshelf.february\": \"Febrero\",\n  \"month.bookshelf.march\": \"Marzo\",\n  \"month.bookshelf.april\": \"Abril\",\n  \"month.bookshelf.may\": \"Mayo\",\n  \"month.bookshelf.june\": \"Junio\",\n  \"month.bookshelf.july\": \"Julio\",\n  \"month.bookshelf.august\": \"Agosto\",\n  \"month.bookshelf.september\": \"Septiembre\",\n  \"month.bookshelf.october\": \"Octubre\",\n  \"month.bookshelf.november\": \"Noviembre\",\n  \"month.bookshelf.december\": \"Diciembre\",\n\n  \"__days\": \"Nombres de los días\",\n  \"day.bookshelf.sunday\": \"Domingo\",\n  \"day.bookshelf.monday\": \"Lunes\",\n  \"day.bookshelf.tuesday\": \"Martes\",\n  \"day.bookshelf.wednesday\": \"Miércoles\",\n  \"day.bookshelf.thursday\": \"Jueves\",\n  \"day.bookshelf.friday\": \"Viernes\",\n  \"day.bookshelf.saturday\": \"Sábado\",\n\n  \"__moon_phases\": \"Los nombres de las diferentes fases lunares que aparecen en el juego.\",\n  \"moon.phase.full\": \"Luna Llena\",\n  \"moon.phase.waxing.gibbous\": \"Gibosa Creciente\",\n  \"moon.phase.first.quarter\": \"Cuarto Creciente\",\n  \"moon.phase.waxing.crescent\": \"Luna Creciente\",\n  \"moon.phase.new\": \"Luna Nueva\",\n  \"moon.phase.waning.crescent\": \"Luna Menguante\",\n  \"moon.phase.last.quarter\": \"Cuarto Menguante\",\n  \"moon.phase.waning.gibbous\": \"Gibosa Menguante\",\n\n  \"__fonts\": \"Entradas de idioma no oficiales para las fuentes de texto vanilla.\",\n  \"font.minecraft.default\": \"Predeterminada\",\n  \"font.minecraft.default.desc\": \"La fuente estándar en Minecraft.\",\n  \"font.minecraft.default.preview\": \"El rápido zorro marrón salta sobre el perro perezoso.\",\n  \"font.minecraft.alt\": \"Alfabeto Galáctico Estándar\",\n  \"font.minecraft.alt.desc\": \"Una fuente basada en runas asociada con encantamientos y magia.\",\n  \"font.minecraft.alt.preview\": \"El cachorro de zorro mágico resolvió el acertijo de los dragones avispa\",\n  \"font.minecraft.illageralt\": \"Alfabeto Illager\",\n  \"font.minecraft.illageralt.desc\": \"Una fuente misteriosa usada por los illagers.\",\n  \"font.minecraft.illageralt.preview\": \"Magos gruñones hacen un brebaje tóxico para la reina jovial.\",\n  \"font.minecraft.uniform\": \"Uniforme\",\n  \"font.minecraft.uniform.desc\": \"Una fuente simple sin estilo.\",\n  \"font.minecraft.uniform.preview\": \"El rápido zorro marrón salta sobre el perro perezoso.\",\n\n  \"__Tags\": \"Nombres de Etiquetas para Visores de Recetas\",\n  \"tag.item.bookshelf.creative_tab.minecraft.colored_blocks\": \"Pestaña Bloques de Colores\",\n  \"tag.item.bookshelf.creative_tab.minecraft.food_and_drinks\": \"Pestaña Comida y Bebidas\",\n  \"tag.item.bookshelf.creative_tab.minecraft.spawn_eggs\": \"Pestaña Huevos de Aparición\",\n  \"tag.item.bookshelf.creative_tab.minecraft.redstone_blocks\": \"Pestaña Redstone\",\n  \"tag.item.bookshelf.creative_tab.minecraft.op_blocks\": \"Pestaña OP/Admin\",\n  \"tag.item.bookshelf.creative_tab.minecraft.combat\": \"Pestaña Combate\",\n  \"tag.item.bookshelf.creative_tab.minecraft.building_blocks\": \"Pestaña Bloques de Construcción\",\n  \"tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities\": \"Pestaña Herramientas y Utilitarios\",\n  \"tag.item.bookshelf.creative_tab.minecraft.natural_blocks\": \"Pestaña Bloques Naturales\",\n  \"tag.item.bookshelf.creative_tab.minecraft.functional_blocks\": \"Pestaña Bloques Funcionales\",\n  \"tag.item.bookshelf.creative_tab.minecraft.ingredients\": \"Pestaña Ingredientes\",\n\n  \"tooltips.bookshelf.loot.unknown\": \"Botín Desconocido\",\n  \"tooltips.bookshelf.loot.unknown.desc\": \"Algunos botines no se pueden mostrar en este momento.\",\n  \"tooltips.bookshelf.loot.empty\": \"Botín Vacío\",\n  \"tooltips.bookshelf.loot.empty.desc\": \"¡Es posible que no caiga nada!\",\n  \"tooltips.bookshelf.loot.dynamic\": \"Botín Dinámico\",\n  \"tooltips.bookshelf.loot.dynamic.desc\": \"Algunos botines dependen del contexto.\",\n\n  \"gui.jei.category.loot.name\": \"Tabla de Botín\",\n\n  \"table.minecraft.archaeology.desert_pyramid.name\": \"Pirámide del Desierto\",\n  \"table.minecraft.archaeology.desert_well.name\": \"Pozo del Desierto\",\n  \"table.minecraft.archaeology.ocean_ruin_cold.name\": \"Ruina Oceánica - Fría\",\n  \"table.minecraft.archaeology.ocean_ruin_warm.name\": \"Ruina Oceánica - Cálida\",\n  \"table.minecraft.archaeology.trail_ruins_common.name\": \"Ruinas del Sendero - Común\",\n  \"table.minecraft.archaeology.trail_ruins_rare.name\": \"Ruinas del Sendero - Raro\",\n  \"table.minecraft.chests.abandoned_mineshaft.name\": \"Mina Abandonada\",\n  \"table.minecraft.chests.ancient_city.name\": \"Ciudad Antigua\",\n  \"table.minecraft.chests.ancient_city_ice_box.name\": \"Ciudad Antigua - Caja de Hielo\",\n  \"table.minecraft.chests.bastion_bridge.name\": \"Bastión - Puente\",\n  \"table.minecraft.chests.bastion_hoglin_stable.name\": \"Bastión - Establo de Hoglins\",\n  \"table.minecraft.chests.bastion_other.name\": \"Bastión - Otro\",\n  \"table.minecraft.chests.bastion_treasure.name\": \"Bastión - Tesoro\",\n  \"table.minecraft.chests.buried_treasure.name\": \"Tesoro Enterrado\",\n  \"table.minecraft.chests.desert_pyramid.name\": \"Pirámide del Desierto\",\n  \"table.minecraft.chests.end_city_treasure.name\": \"Tesoro de Ciudad del End\",\n  \"table.minecraft.chests.igloo_chest.name\": \"Cofre de Iglú\",\n  \"table.minecraft.chests.jungle_temple.name\": \"Templo de la Jungla - Cofre\",\n  \"table.minecraft.chests.jungle_temple_dispenser.name\": \"Templo de la Jungla - Dispensador\",\n  \"table.minecraft.chests.nether_bridge.name\": \"Puente del Nether\",\n  \"table.minecraft.chests.pillager_outpost.name\": \"Puesto de Saqueadores\",\n  \"table.minecraft.chests.ruined_portal.name\": \"Portal en Ruinas\",\n  \"table.minecraft.chests.shipwreck_map.name\": \"Mapa de Naufragio\",\n  \"table.minecraft.chests.shipwreck_supply.name\": \"Naufragio - Suministros\",\n  \"table.minecraft.chests.shipwreck_treasure.name\": \"Naufragio - Tesoro\",\n  \"table.minecraft.chests.simple_dungeon.name\": \"Mazmorra\",\n  \"table.minecraft.chests.spawn_bonus_chest.name\": \"Cofre de Bonificación Inicial\",\n  \"table.minecraft.chests.stronghold_corridor.name\": \"Fortaleza - Corredor\",\n  \"table.minecraft.chests.stronghold_crossing.name\": \"Fortaleza - Cruce\",\n  \"table.minecraft.chests.stronghold_library.name\": \"Fortaleza - Biblioteca\",\n  \"table.minecraft.chests.trial_chambers.corridor.name\": \"Cámara de Desafío - Corredor\",\n  \"table.minecraft.chests.trial_chambers.entrance.name\": \"Cámara de Desafío - Entrada\",\n  \"table.minecraft.chests.trial_chambers.intersection.name\": \"Cámara de Desafío - Intersección\",\n  \"table.minecraft.chests.trial_chambers.intersection_barrel.name\": \"Cámara de Desafío - Barril de Intersección\",\n  \"table.minecraft.chests.trial_chambers.reward.name\": \"Cámara de Desafío - Recompensa\",\n  \"table.minecraft.chests.trial_chambers.reward_common.name\": \"Cámara de Desafío - Recompensa Común\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous.name\": \"Cámara de Desafío - Recompensa Ominosa\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_common.name\": \"Cámara de Desafío - Recompensa Ominosa - Común\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_rare.name\": \"Cámara de Desafío - Recompensa Ominosa - Rara\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_unique.name\": \"Cámara de Desafío - Recompensa Ominosa - Única\",\n  \"table.minecraft.chests.trial_chambers.reward_rare.name\": \"Cámara de Desafío - Recompensa Rara\",\n  \"table.minecraft.chests.trial_chambers.reward_unique.name\": \"Cámara de Desafío - Recompensa Única\",\n  \"table.minecraft.chests.trial_chambers.supply.name\": \"Cámara de Desafío - Suministros\",\n  \"table.minecraft.chests.underwater_ruin_big.name\": \"Ruina Subacuática - Grande\",\n  \"table.minecraft.chests.underwater_ruin_small.name\": \"Ruina Subacuática - Pequeña\",\n  \"table.minecraft.chests.village.village_armorer.name\": \"Aldea - Herrero de Armaduras\",\n  \"table.minecraft.chests.village.village_butcher.name\": \"Aldea - Carnicero\",\n  \"table.minecraft.chests.village.village_cartographer.name\": \"Aldea - Cartógrafo\",\n  \"table.minecraft.chests.village.village_desert_house.name\": \"Aldea - Casa del Desierto\",\n  \"table.minecraft.chests.village.village_fisher.name\": \"Aldea - Pescador\",\n  \"table.minecraft.chests.village.village_fletcher.name\": \"Aldea - Flechero\",\n  \"table.minecraft.chests.village.village_mason.name\": \"Aldea - Albañil\",\n  \"table.minecraft.chests.village.village_plains_house.name\": \"Aldea - Casa de la Llanura\",\n  \"table.minecraft.chests.village.village_savanna_house.name\": \"Aldea - Casa de la Sabana\",\n  \"table.minecraft.chests.village.village_shepherd.name\": \"Aldea - Pastor\",\n  \"table.minecraft.chests.village.village_snowy_house.name\": \"Aldea - Casa Nevada\",\n  \"table.minecraft.chests.village.village_taiga_house.name\": \"Aldea - Casa de la Taiga\",\n  \"table.minecraft.chests.village.village_tannery.name\": \"Aldea - Curtidor\",\n  \"table.minecraft.chests.village.village_temple.name\": \"Aldea - Templo\",\n  \"table.minecraft.chests.village.village_toolsmith.name\": \"Aldea - Herrero de Herramientas\",\n  \"table.minecraft.chests.village.village_weaponsmith.name\": \"Aldea - Herrero de Armas\",\n  \"table.minecraft.chests.woodland_mansion.name\": \"Mansión del Bosque\",\n  \"table.minecraft.dispensers.trial_chambers.chamber.name\": \"Cámara de Desafío - Cámara\",\n  \"table.minecraft.dispensers.trial_chambers.corridor.name\": \"Cámara de Desafío - Corredor\",\n  \"table.minecraft.dispensers.trial_chambers.water.name\": \"Cámara de Desafío - Agua\",\n  \"table.minecraft.empty.name\": \"Vacío\",\n  \"table.minecraft.equipment.trial_chamber.name\": \"Cámara de Desafío\",\n  \"table.minecraft.equipment.trial_chamber_melee.name\": \"Cámara de Desafío - Cuerpo a Cuerpo\",\n  \"table.minecraft.equipment.trial_chamber_ranged.name\": \"Cámara de Desafío - A Distancia\",\n  \"table.minecraft.gameplay.cat_morning_gift.name\": \"Gato - Regalo Matutino\",\n  \"table.minecraft.gameplay.fishing.fish.name\": \"Pesca - Pescado\",\n  \"table.minecraft.gameplay.fishing.junk.name\": \"Pesca - Basura\",\n  \"table.minecraft.gameplay.fishing.name\": \"Pesca\",\n  \"table.minecraft.gameplay.fishing.treasure.name\": \"Pesca - Tesoro\",\n  \"table.minecraft.gameplay.hero_of_the_village.armorer_gift.name\": \"Héroe de la Aldea - Herrero de Armaduras\",\n  \"table.minecraft.gameplay.hero_of_the_village.butcher_gift.name\": \"Héroe de la Aldea - Carnicero\",\n  \"table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name\": \"Héroe de la Aldea - Cartógrafo\",\n  \"table.minecraft.gameplay.hero_of_the_village.cleric_gift.name\": \"Héroe de la Aldea - Clérigo\",\n  \"table.minecraft.gameplay.hero_of_the_village.farmer_gift.name\": \"Héroe de la Aldea - Granjero\",\n  \"table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name\": \"Héroe de la Aldea - Pescador\",\n  \"table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name\": \"Héroe de la Aldea - Flechero\",\n  \"table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name\": \"Héroe de la Aldea - Peletero\",\n  \"table.minecraft.gameplay.hero_of_the_village.librarian_gift.name\": \"Héroe de la Aldea - Bibliotecario\",\n  \"table.minecraft.gameplay.hero_of_the_village.mason_gift.name\": \"Héroe de la Aldea - Albañil\",\n  \"table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name\": \"Héroe de la Aldea - Pastor\",\n  \"table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name\": \"Héroe de la Aldea - Herrero de Herramientas\",\n  \"table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name\": \"Héroe de la Aldea - Herrero de Armas\",\n  \"table.minecraft.gameplay.panda_sneeze.name\": \"Estornudo de Panda\",\n  \"table.minecraft.gameplay.piglin_bartering.name\": \"Intercambio con Piglins\",\n  \"table.minecraft.gameplay.sniffer_digging.name\": \"Excavación de Olfateador\",\n  \"table.minecraft.pots.trial_chambers.corridor.name\": \"Cámara de Desafío - Jarrón del Corredor\",\n  \"table.minecraft.shearing.bogged.name\": \"Atascado - Trasquilar\",\n  \"table.minecraft.spawners.ominous.trial_chamber.consumables.name\": \"Cámara de Desafío - Consumibles Ominosos\",\n  \"table.minecraft.spawners.ominous.trial_chamber.key.name\": \"Cámara de Desafío - Llave Ominosa\",\n  \"table.minecraft.spawners.trial_chamber.consumables.name\": \"Cámara de Desafío - Consumibles\",\n  \"table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name\": \"Cámara de Desafío - Botín Ominoso\",\n  \"table.minecraft.spawners.trial_chamber.key.name\": \"Cámara de Desafío - Llave\",\n  \"table.builtin.block_drops\": \"Bloque - %s\"\n}\n"
  },
  {
    "path": "common/src/main/resources/assets/bookshelf/lang/ja_jp.json",
    "content": "{\n  \"__formatting\": \"\",\n  \"format.bookshelf.right\": \"%s：%s\",\n  \"format.bookshelf.center\": \"%s：%s\",\n  \"format.bookshelf.left\": \"%s：%s\",\n  \"format.bookshelf.spaced\": \"%s %s\",\n  \"format.bookshelf.none\": \"%s%s\",\n  \"format.bookshelf.unit_rate\": \"%s／%s\",\n\n  \"__commands\": \"Command Text\",\n  \"commands.bookshelf.hand.error.not_air\": \"アイテムは空または空気であってはいけません\",\n  \"commands.bookshelf.hand.error.internal\": \"テキストの書式設定中にエラーが発生しました。詳細はログを確認してください\",\n  \"commands.bookshelf.font.unsupported_block\": \"「%s」にフォントを適用できませんでした。この種類のブロックはサポートされていません\",\n  \"commands.bookshelf.font.bad_sender\": \"フォント名変更コマンド\",\n  \"commands.bookshelf.debug.no_info\": \"このコマンドにデバッグ情報はありません\",\n  \"commands.bookshelf.debug.yes_info\": \"デバッグ情報がゲームのログに記録されました。クリックして出力をコピーします\",\n  \"commands.bookshelf.debug.too_long\": \"デバッグ出力が長すぎるためチャットに表示できません。ゲームのログを確認してください\",\n  \"commands.bookshelf.structure.error.no_structures\": \"構造物が見つかりませんでした\",\n  \"commands.bookshelf.structure.found\": \"%s個の構造物を発見しました！\",\n  \n  \"__time_units\": \"Entries for various time units that may be displayed in game.\",\n  \"units.bookshelf.tick\": \"ティック\",\n  \"units.bookshelf.tick.plural\": \"ティック\",\n  \"units.bookshelf.tick.abbreviated\": \"t\",\n  \"units.bookshelf.nanosecond\": \"ナノ秒\",\n  \"units.bookshelf.nanosecond.plural\": \"ナノ秒\",\n  \"units.bookshelf.nanosecond.abbreviated\": \"ns\",\n  \"units.bookshelf.millisecond\": \"ミリ秒\",\n  \"units.bookshelf.millisecond.plural\": \"ミリ秒\",\n  \"units.bookshelf.millisecond.abbreviated\": \"ms\",\n  \"units.bookshelf.second\": \"秒\",\n  \"units.bookshelf.second.plural\": \"秒\",\n  \"units.bookshelf.second.abbreviated\": \"s\",\n  \"units.bookshelf.minute\": \"分\",\n  \"units.bookshelf.minute.plural\": \"分\",\n  \"units.bookshelf.minute.abbreviated\": \"m\",\n  \"units.bookshelf.hour\": \"時間\",\n  \"units.bookshelf.hour.plural\": \"時間\",\n  \"units.bookshelf.hour.abbreviated\": \"h\",\n  \"units.bookshelf.day\": \"日\",\n  \"units.bookshelf.day.plural\": \"日\",\n  \"units.bookshelf.day.abbreviated\": \"d\",\n  \"units.bookshelf.week\": \"週\",\n  \"units.bookshelf.week.plural\": \"週\",\n  \"units.bookshelf.week.abbreviated\": \"wk\",\n  \"units.bookshelf.month\": \"月\",\n  \"units.bookshelf.month.plural\": \"月\",\n  \"units.bookshelf.month.abbreviated\": \"mo\",\n  \"units.bookshelf.year\": \"年\",\n  \"units.bookshelf.year.plural\": \"年\",\n  \"units.bookshelf.year.abbreviated\": \"yr\",\n\n  \"__months\": \"Names of the months\",\n  \"month.bookshelf.january\": \"1月\",\n  \"month.bookshelf.february\": \"2月\",\n  \"month.bookshelf.march\": \"3月\",\n  \"month.bookshelf.april\": \"4月\",\n  \"month.bookshelf.may\": \"5月\",\n  \"month.bookshelf.june\": \"6月\",\n  \"month.bookshelf.july\": \"7月\",\n  \"month.bookshelf.august\": \"8月\",\n  \"month.bookshelf.september\": \"9月\",\n  \"month.bookshelf.october\": \"10月\",\n  \"month.bookshelf.november\": \"11月\",\n  \"month.bookshelf.december\": \"12月\",\n\n  \"__days\": \"Names of the days\",\n  \"day.bookshelf.sunday\": \"日曜日\",\n  \"day.bookshelf.monday\": \"月曜日\",\n  \"day.bookshelf.tuesday\": \"火曜日\",\n  \"day.bookshelf.wednesday\": \"水曜日\",\n  \"day.bookshelf.thursday\": \"木曜日\",\n  \"day.bookshelf.friday\": \"金曜日\",\n  \"day.bookshelf.saturday\": \"土曜日\",\n\n  \"__moon_phases\": \"The names of different moon phases that appear in game.\",\n  \"moon.phase.full\": \"満月\",\n  \"moon.phase.waxing.gibbous\": \"十三夜\",\n  \"moon.phase.first.quarter\": \"上弦\",\n  \"moon.phase.waxing.crescent\": \"三日月\",\n  \"moon.phase.new\": \"新月\",\n  \"moon.phase.waning.crescent\": \"二十五夜\",\n  \"moon.phase.last.quarter\": \"下弦\",\n  \"moon.phase.waning.gibbous\": \"十八夜\",\n\n  \"__fonts\": \"Unofficial language entries for the vanilla text fonts.\",\n  \"font.minecraft.default\": \"デフォルト\",\n  \"font.minecraft.default.desc\": \"Minecraftの標準フォント\",\n  \"font.minecraft.default.preview\": \"The quick brown fox jumps over the lazy dog.\",\n  \"font.minecraft.alt\": \"標準銀河系アルファベット\",\n  \"font.minecraft.alt.desc\": \"エンチャントや魔術に関連するルーン文字ベースのフォント\",\n  \"font.minecraft.alt.preview\": \"Majik fox cub solved the waspy dragons quiz\",\n  \"font.minecraft.illageralt\": \"Illager Alphabet\",\n  \"font.minecraft.illageralt.desc\": \"邪悪な村人が使う謎の多いフォント\",\n  \"font.minecraft.illageralt.preview\": \"Grumpy wizards make a toxic brew for the jovial queen.\",\n  \"font.minecraft.uniform\": \"Uniform\",\n  \"font.minecraft.uniform.desc\": \"様式化されていないプレーンなフォント\",\n  \"font.minecraft.uniform.preview\": \"The quick brown fox jumps over the lazy dog.\",\n\n  \"__Tags\": \"Tag Names for Recipe Viewers\",\n  \"tag.item.bookshelf.creative_tab.minecraft.colored_blocks\": \"色付きブロックタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.food_and_drinks\": \"食べ物と飲み物タブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.spawn_eggs\": \"スポーンエッグタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.redstone_blocks\": \"レッドストーン系ブロックタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.op_blocks\": \"管理者用アイテムタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.combat\": \"戦闘タブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.building_blocks\": \"建築ブロックタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities\": \"道具と実用品タブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.natural_blocks\": \"天然ブロックタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.functional_blocks\": \"機能的ブロックタブ\",\n  \"tag.item.bookshelf.creative_tab.minecraft.ingredients\": \"材料タブ\",\n\n  \"tooltips.bookshelf.loot.unknown\": \"不明なドロップ\",\n  \"tooltips.bookshelf.loot.unknown.desc\": \"現在、一部のドロップアイテムを表示できません\",\n  \"tooltips.bookshelf.loot.empty\": \"空のドロップ\",\n  \"tooltips.bookshelf.loot.empty.desc\": \"何もドロップしない可能性があります！\",\n  \"tooltips.bookshelf.loot.dynamic\": \"動的ドロップ\",\n  \"tooltips.bookshelf.loot.dynamic.desc\": \"特定の条件に依存したドロップアイテムがあります\",\n\n  \"gui.jei.category.loot.name\": \"ルートテーブル\",\n\n  \"table.minecraft.archaeology.desert_pyramid.name\": \"砂漠の寺院\",\n  \"table.minecraft.archaeology.desert_well.name\": \"砂漠の井戸\",\n  \"table.minecraft.archaeology.ocean_ruin_cold.name\": \"海底遺跡 - 冷たい海域\",\n  \"table.minecraft.archaeology.ocean_ruin_warm.name\": \"海底遺跡 - 暖かい海域\",\n  \"table.minecraft.archaeology.trail_ruins_common.name\": \"旅路の遺跡 - 普通\",\n  \"table.minecraft.archaeology.trail_ruins_rare.name\": \"旅路の遺跡 - 稀少品\",\n  \"table.minecraft.chests.abandoned_mineshaft.name\": \"廃坑\",\n  \"table.minecraft.chests.ancient_city.name\": \"古代都市\",\n  \"table.minecraft.chests.ancient_city_ice_box.name\": \"古代都市 - 氷室\",\n  \"table.minecraft.chests.bastion_bridge.name\": \"砦の遺跡 - 橋\",\n  \"table.minecraft.chests.bastion_hoglin_stable.name\": \"砦の遺跡 - ホグリンの小屋\",\n  \"table.minecraft.chests.bastion_other.name\": \"砦の遺跡 - 一般\",\n  \"table.minecraft.chests.bastion_treasure.name\": \"砦の遺跡 - 宝物部屋\",\n  \"table.minecraft.chests.buried_treasure.name\": \"埋もれた宝\",\n  \"table.minecraft.chests.desert_pyramid.name\": \"砂漠の寺院\",\n  \"table.minecraft.chests.end_city_treasure.name\": \"エンドシティ - 宝物\",\n  \"table.minecraft.chests.igloo_chest.name\": \"イグルー - チェスト\",\n  \"table.minecraft.chests.jungle_temple.name\": \"ジャングルの寺院 - チェスト\",\n  \"table.minecraft.chests.jungle_temple_dispenser.name\": \"ジャングルの寺院 - ディスペンサー\",\n  \"table.minecraft.chests.nether_bridge.name\": \"ネザー要塞\",\n  \"table.minecraft.chests.pillager_outpost.name\": \"ピリジャーの前哨基地\",\n  \"table.minecraft.chests.ruined_portal.name\": \"荒廃したポータル\",\n  \"table.minecraft.chests.shipwreck_map.name\": \"難破船 - 地図入り\",\n  \"table.minecraft.chests.shipwreck_supply.name\": \"難破船 - 補給物資\",\n  \"table.minecraft.chests.shipwreck_treasure.name\": \"難破船 - 宝物\",\n  \"table.minecraft.chests.simple_dungeon.name\": \"ダンジョン\",\n  \"table.minecraft.chests.spawn_bonus_chest.name\": \"ボーナスチェスト\",\n  \"table.minecraft.chests.stronghold_corridor.name\": \"要塞 - 祭壇\",\n  \"table.minecraft.chests.stronghold_crossing.name\": \"要塞 - 倉庫\",\n  \"table.minecraft.chests.stronghold_library.name\": \"要塞 - 図書室\",\n  \"table.minecraft.chests.trial_chambers.corridor.name\": \"試練の間 - 廊下\",\n  \"table.minecraft.chests.trial_chambers.entrance.name\": \"試練の間 - 玄関\",\n  \"table.minecraft.chests.trial_chambers.intersection.name\": \"試練の間 - 交差の部屋\",\n  \"table.minecraft.chests.trial_chambers.intersection_barrel.name\": \"試練の間 - 交差の部屋の樽\",\n  \"table.minecraft.chests.trial_chambers.reward.name\": \"試練の間 - 報酬\",\n  \"table.minecraft.chests.trial_chambers.reward_common.name\": \"試練の間 - 通常の報酬\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous.name\": \"試練の間 - 不吉な報酬\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_common.name\": \"試練の間 - 不吉な報酬 - 普通\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_rare.name\": \"試練の間 - 不吉な報酬 - 希少品\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_unique.name\": \"試練の間 - 不吉な報酬 - ユニーク\",\n  \"table.minecraft.chests.trial_chambers.reward_rare.name\": \"試練の間 - 希少な報酬\",\n  \"table.minecraft.chests.trial_chambers.reward_unique.name\": \"試練の間 - ユニークな報酬\",\n  \"table.minecraft.chests.trial_chambers.supply.name\": \"試練の間 - 補給物資\",\n  \"table.minecraft.chests.underwater_ruin_big.name\": \"海底遺跡 - 大\",\n  \"table.minecraft.chests.underwater_ruin_small.name\": \"海底遺跡 - 小\",\n  \"table.minecraft.chests.village.village_armorer.name\": \"村 - 防具鍛冶の家\",\n  \"table.minecraft.chests.village.village_butcher.name\": \"村 - 肉屋の店\",\n  \"table.minecraft.chests.village.village_cartographer.name\": \"村 - 製図家の家\",\n  \"table.minecraft.chests.village.village_desert_house.name\": \"村 - 砂漠の村の家\",\n  \"table.minecraft.chests.village.village_fisher.name\": \"村 - 釣り小屋\",\n  \"table.minecraft.chests.village.village_fletcher.name\": \"村 - 矢師の家\",\n  \"table.minecraft.chests.village.village_mason.name\": \"村 - 石工の家\",\n  \"table.minecraft.chests.village.village_plains_house.name\": \"村 - 平原の村の家\",\n  \"table.minecraft.chests.village.village_savanna_house.name\": \"村 - サバンナの村の家\",\n  \"table.minecraft.chests.village.village_shepherd.name\": \"村 - 羊飼いの家\",\n  \"table.minecraft.chests.village.village_snowy_house.name\": \"村 - 雪原の村の家\",\n  \"table.minecraft.chests.village.village_taiga_house.name\": \"村 - タイガの村の家\",\n  \"table.minecraft.chests.village.village_tannery.name\": \"村 - 革加工場\",\n  \"table.minecraft.chests.village.village_temple.name\": \"村 - 礼拝所\",\n  \"table.minecraft.chests.village.village_toolsmith.name\": \"村 - 道具鍛冶場\",\n  \"table.minecraft.chests.village.village_weaponsmith.name\": \"村 - 武器鍛冶場\",\n  \"table.minecraft.chests.woodland_mansion.name\": \"森の洋館\",\n  \"table.minecraft.dispensers.trial_chambers.chamber.name\": \"試練の間 - 試練室\",\n  \"table.minecraft.dispensers.trial_chambers.corridor.name\": \"試練の間 - 廊下\",\n  \"table.minecraft.dispensers.trial_chambers.water.name\": \"試練の間 - 水\",\n  \"table.minecraft.empty.name\": \"空\",\n  \"table.minecraft.equipment.trial_chamber.name\": \"試練の間\",\n  \"table.minecraft.equipment.trial_chamber_melee.name\": \"試練の間 - 近接攻撃型\",\n  \"table.minecraft.equipment.trial_chamber_ranged.name\": \"試練の間 - 遠隔攻撃型\",\n  \"table.minecraft.gameplay.cat_morning_gift.name\": \"ネコ - 贈り物\",\n  \"table.minecraft.gameplay.fishing.fish.name\": \"釣り - 魚\",\n  \"table.minecraft.gameplay.fishing.junk.name\": \"釣り - ゴミ\",\n  \"table.minecraft.gameplay.fishing.name\": \"釣り\",\n  \"table.minecraft.gameplay.fishing.treasure.name\": \"釣り - 宝\",\n  \"table.minecraft.gameplay.hero_of_the_village.armorer_gift.name\": \"村の英雄 - 防具鍛冶\",\n  \"table.minecraft.gameplay.hero_of_the_village.butcher_gift.name\": \"村の英雄 - 肉屋\",\n  \"table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name\": \"村の英雄 - 製図家\",\n  \"table.minecraft.gameplay.hero_of_the_village.cleric_gift.name\": \"村の英雄 - 聖職者\",\n  \"table.minecraft.gameplay.hero_of_the_village.farmer_gift.name\": \"村の英雄 - 農民\",\n  \"table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name\": \"村の英雄 - 釣り人\",\n  \"table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name\": \"村の英雄 - 矢師\",\n  \"table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name\": \"村の英雄 - 革細工師\",\n  \"table.minecraft.gameplay.hero_of_the_village.librarian_gift.name\": \"村の英雄 - 司書\",\n  \"table.minecraft.gameplay.hero_of_the_village.mason_gift.name\": \"村の英雄 - 石工\",\n  \"table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name\": \"村の英雄 - 羊飼い\",\n  \"table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name\": \"村の英雄 - 道具鍛冶\",\n  \"table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name\": \"村の英雄 - 武器鍛冶\",\n  \"table.minecraft.gameplay.panda_sneeze.name\": \"パンダのくしゃみ\",\n  \"table.minecraft.gameplay.piglin_bartering.name\": \"ピグリンとの物々交換\",\n  \"table.minecraft.gameplay.sniffer_digging.name\": \"スニッファーが掘り出す\",\n  \"table.minecraft.pots.trial_chambers.corridor.name\": \"試練の間 - 廊下の飾り壺\",\n  \"table.minecraft.shearing.bogged.name\": \"ボグド - 刈り取り\",\n  \"table.minecraft.spawners.ominous.trial_chamber.consumables.name\": \"試練の間 - 不吉な消耗品\",\n  \"table.minecraft.spawners.ominous.trial_chamber.key.name\": \"試練の間 - 不吉な鍵\",\n  \"table.minecraft.spawners.trial_chamber.consumables.name\": \"試練の間 - 消耗品\",\n  \"table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name\": \"試練の間 - 不吉なアイテムスポナー\",\n  \"table.minecraft.spawners.trial_chamber.key.name\": \"試練の間 - 鍵\",\n  \"table.builtin.block_drops\": \"ブロック - %s\"\n}\n"
  },
  {
    "path": "common/src/main/resources/assets/bookshelf/lang/pt_br.json",
    "content": "{\n  \"__formatting\": \"\",\n  \"format.bookshelf.right\": \"%s: %s\",\n  \"format.bookshelf.center\": \"%s : %s\",\n  \"format.bookshelf.left\": \"%s :%s\",\n  \"format.bookshelf.spaced\": \"%s %s\",\n  \"format.bookshelf.none\": \"%s%s\",\n  \"format.bookshelf.unit_rate\": \"%s/%s\",\n\n  \"__commands\": \"Command Text\",\n  \"commands.bookshelf.hand.error.not_air\": \"O item não deve estar vazio ou ser ar!\",\n  \"commands.bookshelf.hand.error.internal\": \"Erro encontrado ao formatar o texto. Verifique os logs para mais informações.\",\n  \"commands.bookshelf.font.unsupported_block\": \"Não foi possível aplicar a fonte a '%s'. Este tipo de bloco não é suportado.\",\n  \"commands.bookshelf.font.bad_sender\": \"O comando de renomear fonte\",\n  \"commands.bookshelf.debug.no_info\": \"Nenhuma informação de depuração disponível para este comando.\",\n  \"commands.bookshelf.debug.yes_info\": \"As informações de depuração foram registradas no seu log do jogo. Clique para copiar a saída.\",\n  \"commands.bookshelf.debug.too_long\": \"A saída de depuração é muito grande para o chat. Por favor, verifique o seu log do jogo.\",\n  \"commands.bookshelf.structure.error.no_structures\": \"Nenhuma estrutura pôde ser encontrada.\",\n  \"commands.bookshelf.structure.found\": \"Encontrada(s) %s estrutura(s)!\",\n\n  \"__time_units\": \"Entries for various time units that may be displayed in game.\",\n  \"units.bookshelf.tick\": \"Tick\",\n  \"units.bookshelf.tick.plural\": \"Ticks\",\n  \"units.bookshelf.tick.abbreviated\": \"t\",\n  \"units.bookshelf.nanosecond\": \"Nanossegundo\",\n  \"units.bookshelf.nanosecond.plural\": \"Nanossegundos\",\n  \"units.bookshelf.nanosecond.abbreviated\": \"ns\",\n  \"units.bookshelf.millisecond\": \"Milissegundo\",\n  \"units.bookshelf.millisecond.plural\": \"Milissegundos\",\n  \"units.bookshelf.millisecond.abbreviated\": \"ms\",\n  \"units.bookshelf.second\": \"Segundo\",\n  \"units.bookshelf.second.plural\": \"Segundos\",\n  \"units.bookshelf.second.abbreviated\": \"s\",\n  \"units.bookshelf.minute\": \"Minuto\",\n  \"units.bookshelf.minute.plural\": \"Minutos\",\n  \"units.bookshelf.minute.abbreviated\": \"m\",\n  \"units.bookshelf.hour\": \"Hora\",\n  \"units.bookshelf.hour.plural\": \"Horas\",\n  \"units.bookshelf.hour.abbreviated\": \"h\",\n  \"units.bookshelf.day\": \"Dia\",\n  \"units.bookshelf.day.plural\": \"Dias\",\n  \"units.bookshelf.day.abbreviated\": \"d\",\n  \"units.bookshelf.week\": \"Semana\",\n  \"units.bookshelf.week.plural\": \"Semanas\",\n  \"units.bookshelf.week.abbreviated\": \"sem\",\n  \"units.bookshelf.month\": \"Mês\",\n  \"units.bookshelf.month.plural\": \"Meses\",\n  \"units.bookshelf.month.abbreviated\": \"mês\",\n  \"units.bookshelf.year\": \"Ano\",\n  \"units.bookshelf.year.plural\": \"Anos\",\n  \"units.bookshelf.year.abbreviated\": \"a\",\n\n  \"__months\": \"Names of the months\",\n  \"month.bookshelf.january\": \"Janeiro\",\n  \"month.bookshelf.february\": \"Fevereiro\",\n  \"month.bookshelf.march\": \"Março\",\n  \"month.bookshelf.april\": \"Abril\",\n  \"month.bookshelf.may\": \"Maio\",\n  \"month.bookshelf.june\": \"Junho\",\n  \"month.bookshelf.july\": \"Julho\",\n  \"month.bookshelf.august\": \"Agosto\",\n  \"month.bookshelf.september\": \"Setembro\",\n  \"month.bookshelf.october\": \"Outubro\",\n  \"month.bookshelf.november\": \"Novembro\",\n  \"month.bookshelf.december\": \"Dezembro\",\n\n  \"__days\": \"Names of the days\",\n  \"day.bookshelf.sunday\": \"Domingo\",\n  \"day.bookshelf.monday\": \"Segunda-feira\",\n  \"day.bookshelf.tuesday\": \"Terça-feira\",\n  \"day.bookshelf.wednesday\": \"Quarta-feira\",\n  \"day.bookshelf.thursday\": \"Quinta-feira\",\n  \"day.bookshelf.friday\": \"Sexta-feira\",\n  \"day.bookshelf.saturday\": \"Sábado\",\n\n  \"__moon_phases\": \"The names of different moon phases that appear in game.\",\n  \"moon.phase.full\": \"Lua Cheia\",\n  \"moon.phase.waxing.gibbous\": \"Gibosa Crescente\",\n  \"moon.phase.first.quarter\": \"Quarto Crescente\",\n  \"moon.phase.waxing.crescent\": \"Lua Crescente\",\n  \"moon.phase.new\": \"Lua Nova\",\n  \"moon.phase.waning.crescent\": \"Lua Minguante\",\n  \"moon.phase.last.quarter\": \"Quarto Minguante\",\n  \"moon.phase.waning.gibbous\": \"Gibosa Minguante\",\n\n  \"__fonts\": \"Unofficial language entries for the vanilla text fonts.\",\n  \"font.minecraft.default\": \"Padrão\",\n  \"font.minecraft.default.desc\": \"A fonte padrão do Minecraft.\",\n  \"font.minecraft.default.preview\": \"A rápida raposa marrom salta sobre o cão preguiçoso.\",\n  \"font.minecraft.alt\": \"Alfabeto Galáctico Padrão\",\n  \"font.minecraft.alt.desc\": \"Uma fonte baseada em runas associada a encantamentos e magia.\",\n  \"font.minecraft.alt.preview\": \"O filhote de raposa mágico resolveu o enigma dos dragões vespertinos\",\n  \"font.minecraft.illageralt\": \"Alfabeto Illager\",\n  \"font.minecraft.illageralt.desc\": \"Uma fonte misteriosa usada pelos illagers.\",\n  \"font.minecraft.illageralt.preview\": \"Magos mal-humorados fazem uma poção tóxica para a rainha jovial.\",\n  \"font.minecraft.uniform\": \"Uniforme\",\n  \"font.minecraft.uniform.desc\": \"Uma fonte simples que não é estilizada.\",\n  \"font.minecraft.uniform.preview\": \"A rápida raposa marrom salta sobre o cão preguiçoso.\",\n\n  \"__Tags\": \"Tag Names for Recipe Viewers\",\n  \"tag.item.bookshelf.creative_tab.minecraft.colored_blocks\": \"Aba de Blocos Coloridos\",\n  \"tag.item.bookshelf.creative_tab.minecraft.food_and_drinks\": \"Aba de Comidas e Bebidas\",\n  \"tag.item.bookshelf.creative_tab.minecraft.spawn_eggs\": \"Aba de Ovos de Invocação\",\n  \"tag.item.bookshelf.creative_tab.minecraft.redstone_blocks\": \"Aba de Redstone\",\n  \"tag.item.bookshelf.creative_tab.minecraft.op_blocks\": \"Aba de OP/Admin\",\n  \"tag.item.bookshelf.creative_tab.minecraft.combat\": \"Aba de Combate\",\n  \"tag.item.bookshelf.creative_tab.minecraft.building_blocks\": \"Aba de Blocos de Construção\",\n  \"tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities\": \"Aba de Ferramentas e Utilidades\",\n  \"tag.item.bookshelf.creative_tab.minecraft.natural_blocks\": \"Aba de Blocos Naturais\",\n  \"tag.item.bookshelf.creative_tab.minecraft.functional_blocks\": \"Aba de Blocos Funcionais\",\n  \"tag.item.bookshelf.creative_tab.minecraft.ingredients\": \"Aba de Ingredientes\",\n\n  \"tooltips.bookshelf.loot.unknown\": \"Drop Desconhecido\",\n  \"tooltips.bookshelf.loot.unknown.desc\": \"Alguns drops não podem ser exibidos no momento.\",\n  \"tooltips.bookshelf.loot.empty\": \"Drop Vazio\",\n  \"tooltips.bookshelf.loot.empty.desc\": \"É possível que nada seja dropado!\",\n  \"tooltips.bookshelf.loot.dynamic\": \"Drop Dinâmico\",\n  \"tooltips.bookshelf.loot.dynamic.desc\": \"Alguns drops dependem do contexto.\",\n\n  \"gui.jei.category.loot.name\": \"Tabela de Loot\",\n\n  \"table.minecraft.archaeology.desert_pyramid.name\": \"Pirâmide do Deserto\",\n  \"table.minecraft.archaeology.desert_well.name\": \"Poço do Deserto\",\n  \"table.minecraft.archaeology.ocean_ruin_cold.name\": \"Ruína do Oceano - Fria\",\n  \"table.minecraft.archaeology.ocean_ruin_warm.name\": \"Ruína do Oceano - Quente\",\n  \"table.minecraft.archaeology.trail_ruins_common.name\": \"Ruínas de Trilha - Comum\",\n  \"table.minecraft.archaeology.trail_ruins_rare.name\": \"Ruínas de Trilha - Raro\",\n  \"table.minecraft.chests.abandoned_mineshaft.name\": \"Mina Abandonada\",\n  \"table.minecraft.chests.ancient_city.name\": \"Cidade Antiga\",\n  \"table.minecraft.chests.ancient_city_ice_box.name\": \"Caixa de Gelo da Cidade Antiga\",\n  \"table.minecraft.chests.bastion_bridge.name\": \"Bastião - Ponte\",\n  \"table.minecraft.chests.bastion_hoglin_stable.name\": \"Bastião - Estábulo de Hoglins\",\n  \"table.minecraft.chests.bastion_other.name\": \"Bastião - Outro\",\n  \"table.minecraft.chests.bastion_treasure.name\": \"Bastião - Tesouro\",\n  \"table.minecraft.chests.buried_treasure.name\": \"Tesouro Enterrado\",\n  \"table.minecraft.chests.desert_pyramid.name\": \"Pirâmide do Deserto\",\n  \"table.minecraft.chests.end_city_treasure.name\": \"Tesouro da Cidade do Fim\",\n  \"table.minecraft.chests.igloo_chest.name\": \"Baú do Iglu\",\n  \"table.minecraft.chests.jungle_temple.name\": \"Templo da Selva - Baú\",\n  \"table.minecraft.chests.jungle_temple_dispenser.name\": \"Templo da Selva - Ejetor\",\n  \"table.minecraft.chests.nether_bridge.name\": \"Ponte do Nether\",\n  \"table.minecraft.chests.pillager_outpost.name\": \"Posto Avançado de Saqueadores\",\n  \"table.minecraft.chests.ruined_portal.name\": \"Portal em Ruínas\",\n  \"table.minecraft.chests.shipwreck_map.name\": \"Mapa de Naufrágio\",\n  \"table.minecraft.chests.shipwreck_supply.name\": \"Naufrágio - Suprimentos\",\n  \"table.minecraft.chests.shipwreck_treasure.name\": \"Naufrágio - Tesouro\",\n  \"table.minecraft.chests.simple_dungeon.name\": \"Masmorra\",\n  \"table.minecraft.chests.spawn_bonus_chest.name\": \"Baú de Bônus Inicial\",\n  \"table.minecraft.chests.stronghold_corridor.name\": \"Fortaleza - Corredor\",\n  \"table.minecraft.chests.stronghold_crossing.name\": \"Fortaleza - Cruzamento\",\n  \"table.minecraft.chests.stronghold_library.name\": \"Fortaleza - Biblioteca\",\n  \"table.minecraft.chests.trial_chambers.corridor.name\": \"Câmara de Desafios - Corredor\",\n  \"table.minecraft.chests.trial_chambers.entrance.name\": \"Câmara de Desafios - Entrada\",\n  \"table.minecraft.chests.trial_chambers.intersection.name\": \"Câmara de Desafios - Interseção\",\n  \"table.minecraft.chests.trial_chambers.intersection_barrel.name\": \"Câmara de Desafios - Barril da Interseção\",\n  \"table.minecraft.chests.trial_chambers.reward.name\": \"Câmara de Desafios - Recompensa\",\n  \"table.minecraft.chests.trial_chambers.reward_common.name\": \"Câmara de Desafios - Recompensa Comum\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous.name\": \"Câmara de Desafios - Recompensa Sinistra\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_common.name\": \"Câmara de Desafios - Recompensa Sinistra - Comum\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_rare.name\": \"Câmara de Desafios - Recompensa Sinistra - Rara\",\n  \"table.minecraft.chests.trial_chambers.reward_ominous_unique.name\": \"Câmara de Desafios - Recompensa Sinistra - Única\",\n  \"table.minecraft.chests.trial_chambers.reward_rare.name\": \"Câmara de Desafios - Recompensa Rara\",\n  \"table.minecraft.chests.trial_chambers.reward_unique.name\": \"Câmara de Desafios - Recompensa Única\",\n  \"table.minecraft.chests.trial_chambers.supply.name\": \"Câmara de Desafios - Suprimentos\",\n  \"table.minecraft.chests.underwater_ruin_big.name\": \"Ruína Subaquática - Grande\",\n  \"table.minecraft.chests.underwater_ruin_small.name\": \"Ruína Subaquática - Pequena\",\n  \"table.minecraft.chests.village.village_armorer.name\": \"Vila - Armeiro\",\n  \"table.minecraft.chests.village.village_butcher.name\": \"Vila - Açougueiro\",\n  \"table.minecraft.chests.village.village_cartographer.name\": \"Vila - Cartógrafo\",\n  \"table.minecraft.chests.village.village_desert_house.name\": \"Vila - Casa do Deserto\",\n  \"table.minecraft.chests.village.village_fisher.name\": \"Vila - Pescador\",\n  \"table.minecraft.chests.village.village_fletcher.name\": \"Vila - Flecheiro\",\n  \"table.minecraft.chests.village.village_mason.name\": \"Vila - Pedreiro\",\n  \"table.minecraft.chests.village.village_plains_house.name\": \"Vila - Casa da Planície\",\n  \"table.minecraft.chests.village.village_savanna_house.name\": \"Vila - Casa da Savana\",\n  \"table.minecraft.chests.village.village_shepherd.name\": \"Vila - Pastor\",\n  \"table.minecraft.chests.village.village_snowy_house.name\": \"Vila - Casa de Neve\",\n  \"table.minecraft.chests.village.village_taiga_house.name\": \"Vila - Casa da Taiga\",\n  \"table.minecraft.chests.village.village_tannery.name\": \"Vila - Curtume\",\n  \"table.minecraft.chests.village.village_temple.name\": \"Vila - Templo\",\n  \"table.minecraft.chests.village.village_toolsmith.name\": \"Vila - Ferreiro (Ferramentas)\",\n  \"table.minecraft.chests.village.village_weaponsmith.name\": \"Vila - Ferreiro (Armas)\",\n  \"table.minecraft.chests.woodland_mansion.name\": \"Mansão da Floresta\",\n  \"table.minecraft.dispensers.trial_chambers.chamber.name\": \"Câmara de Desafios - Câmara\",\n  \"table.minecraft.dispensers.trial_chambers.corridor.name\": \"Câmara de Desafios - Corredor\",\n  \"table.minecraft.dispensers.trial_chambers.water.name\": \"Câmara de Desafios - Água\",\n  \"table.minecraft.empty.name\": \"Vazio\",\n  \"table.minecraft.equipment.trial_chamber.name\": \"Câmara de Desafios\",\n  \"table.minecraft.equipment.trial_chamber_melee.name\": \"Câmara de Desafios - Corpo a Corpo\",\n  \"table.minecraft.equipment.trial_chamber_ranged.name\": \"Câmara de Desafios - À Distância\",\n  \"table.minecraft.gameplay.cat_morning_gift.name\": \"Gato - Presente Matinal\",\n  \"table.minecraft.gameplay.fishing.fish.name\": \"Pesca - Peixe\",\n  \"table.minecraft.gameplay.fishing.junk.name\": \"Pesca - Lixo\",\n  \"table.minecraft.gameplay.fishing.name\": \"Pesca\",\n  \"table.minecraft.gameplay.fishing.treasure.name\": \"Pesca - Tesouro\",\n  \"table.minecraft.gameplay.hero_of_the_village.armorer_gift.name\": \"Herói da Vila - Armeiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.butcher_gift.name\": \"Herói da Vila - Açougueiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name\": \"Herói da Vila - Cartógrafo\",\n  \"table.minecraft.gameplay.hero_of_the_village.cleric_gift.name\": \"Herói da Vila - Clérigo\",\n  \"table.minecraft.gameplay.hero_of_the_village.farmer_gift.name\": \"Herói da Vila - Fazendeiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name\": \"Herói da Vila - Pescador\",\n  \"table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name\": \"Herói da Vila - Flecheiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name\": \"Herói da Vila - Coureiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.librarian_gift.name\": \"Herói da Vila - Bibliotecário\",\n  \"table.minecraft.gameplay.hero_of_the_village.mason_gift.name\": \"Herói da Vila - Pedreiro\",\n  \"table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name\": \"Herói da Vila - Pastor\",\n  \"table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name\": \"Herói da Vila - Ferreiro (Ferramentas)\",\n  \"table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name\": \"Herói da Vila - Ferreiro (Armas)\",\n  \"table.minecraft.gameplay.panda_sneeze.name\": \"Espirro de Panda\",\n  \"table.minecraft.gameplay.piglin_bartering.name\": \"Troca com Piglins\",\n  \"table.minecraft.gameplay.sniffer_digging.name\": \"Escavação do Farejador\",\n  \"table.minecraft.pots.trial_chambers.corridor.name\": \"Câmara de Desafios - Vaso do Corredor\",\n  \"table.minecraft.shearing.bogged.name\": \"Atolado - Tosquia\",\n  \"table.minecraft.spawners.ominous.trial_chamber.consumables.name\": \"Câmara de Desafios - Consumíveis Sinistros\",\n  \"table.minecraft.spawners.ominous.trial_chamber.key.name\": \"Câmara de Desafios - Chave Sinistra\",\n  \"table.minecraft.spawners.trial_chamber.consumables.name\": \"Câmara de Desafios - Consumíveis\",\n  \"table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name\": \"Câmara de Desafios - Drops Sinistros\",\n  \"table.minecraft.spawners.trial_chamber.key.name\": \"Câmara de Desafios - Chave\",\n  \"table.builtin.block_drops\": \"Bloco - %s\"\n}\n"
  },
  {
    "path": "common/src/main/resources/assets/bookshelf/lang/zh_cn.json",
    "content": "{\n  \"__formatting\": \"\",\n  \"format.bookshelf.right\": \"%s: %s\",\n  \"format.bookshelf.center\": \"%s : %s\",\n  \"format.bookshelf.left\": \"%s :%s\",\n  \"format.bookshelf.spaced\": \"%s %s\",\n  \"format.bookshelf.none\": \"%s%s\",\n  \"format.bookshelf.unit_rate\": \"%s/%s\",\n\n  \"__commands\": \"命令文本\",\n  \"commands.bookshelf.hand.error.not_air\": \"物品不能为空或空气！\",\n  \"commands.bookshelf.font.unsupported_block\": \"无法将字体应用到 '%s'。不支持这种方块\",\n  \"commands.bookshelf.font.bad_sender\": \"字体重命名命令\",\n\n  \"__time_units\": \"游戏中可能显示的各种时间单位的条目\",\n  \"units.bookshelf.tick\": \"刻\",\n  \"units.bookshelf.tick.plural\": \"刻\",\n  \"units.bookshelf.tick.abbreviated\": \"t\",\n  \"units.bookshelf.nanosecond\": \"纳秒\",\n  \"units.bookshelf.nanosecond.plural\": \"纳秒\",\n  \"units.bookshelf.nanosecond.abbreviated\": \"ns\",\n  \"units.bookshelf.millisecond\": \"毫秒\",\n  \"units.bookshelf.millisecond.plural\": \"毫秒\",\n  \"units.bookshelf.millisecond.abbreviated\": \"ms\",\n  \"units.bookshelf.second\": \"秒\",\n  \"units.bookshelf.second.plural\": \"秒\",\n  \"units.bookshelf.second.abbreviated\": \"s\",\n  \"units.bookshelf.minute\": \"分钟\",\n  \"units.bookshelf.minute.plural\": \"分钟\",\n  \"units.bookshelf.minute.abbreviated\": \"m\",\n  \"units.bookshelf.hour\": \"小时\",\n  \"units.bookshelf.hour.plural\": \"小时\",\n  \"units.bookshelf.hour.abbreviated\": \"h\",\n  \"units.bookshelf.day\": \"日\",\n  \"units.bookshelf.day.plural\": \"日\",\n  \"units.bookshelf.day.abbreviated\": \"d\",\n  \"units.bookshelf.week\": \"周\",\n  \"units.bookshelf.week.plural\": \"周\",\n  \"units.bookshelf.week.abbreviated\": \"wk\",\n  \"units.bookshelf.month\": \"月\",\n  \"units.bookshelf.month.plural\": \"月\",\n  \"units.bookshelf.month.abbreviated\": \"mo\",\n  \"units.bookshelf.year\": \"年\",\n  \"units.bookshelf.year.plural\": \"年\",\n  \"units.bookshelf.year.abbreviated\": \"yr\",\n\n  \"__months\": \"月份名称\",\n  \"month.bookshelf.january\": \"一月\",\n  \"month.bookshelf.february\": \"二月\",\n  \"month.bookshelf.march\": \"三月\",\n  \"month.bookshelf.april\": \"四月\",\n  \"month.bookshelf.may\": \"五月\",\n  \"month.bookshelf.june\": \"六月\",\n  \"month.bookshelf.july\": \"七月\",\n  \"month.bookshelf.august\": \"八月\",\n  \"month.bookshelf.september\": \"九月\",\n  \"month.bookshelf.october\": \"十月\",\n  \"month.bookshelf.november\": \"十一月\",\n  \"month.bookshelf.december\": \"十二月\",\n\n  \"__days\": \"日期名称\",\n  \"day.bookshelf.sunday\": \"星期日\",\n  \"day.bookshelf.monday\": \"星期一\",\n  \"day.bookshelf.tuesday\": \"星期二\",\n  \"day.bookshelf.wednesday\": \"星期三\",\n  \"day.bookshelf.thursday\": \"星期四\",\n  \"day.bookshelf.friday\": \"星期五\",\n  \"day.bookshelf.saturday\": \"星期六\",\n\n  \"__moon_phases\": \"游戏中出现的不同月相的名称\",\n  \"moon.phase.full\": \"满月\",\n  \"moon.phase.waxing.gibbous\": \"盈凸月\",\n  \"moon.phase.first.quarter\": \"上弦月\",\n  \"moon.phase.waxing.crescent\": \"蛾眉月\",\n  \"moon.phase.new\": \"新月\",\n  \"moon.phase.waning.crescent\": \"残月\",\n  \"moon.phase.last.quarter\": \"下弦月\",\n  \"moon.phase.waning.gibbous\": \"亏凸月\",\n\n  \"__fonts\": \"原版字体的非官方语言条目\",\n  \"font.minecraft.default\": \"默认\",\n  \"font.minecraft.default.desc\": \"Minecraft 中的标准字体\",\n  \"font.minecraft.default.preview\": \"The quick brown fox jumps over the lazy dog.\",\n  \"font.minecraft.alt\": \"标准星系字母\",\n  \"font.minecraft.alt.desc\": \"一种基于符文的字体，与魔法和魔法有关\",\n  \"font.minecraft.alt.preview\": \"Majik fox cub solved the waspy dragons quiz\",\n  \"font.minecraft.illageralt\": \"灾厄村民\",\n  \"font.minecraft.illageralt.desc\": \"灾厄村民使用的神秘字体\",\n  \"font.minecraft.illageralt.preview\": \"Grumpy wizards make a toxic brew for the jovial queen.\",\n  \"font.minecraft.uniform\": \"统一字体\",\n  \"font.minecraft.uniform.desc\": \"一种没有风格的普通字体\",\n  \"font.minecraft.uniform.preview\": \"The quick brown fox jumps over the lazy dog.\"\n}\n"
  },
  {
    "path": "common/src/main/resources/bookshelf.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8\",\n  \"package\": \"net.darkhax.bookshelf.common.mixin\",\n  \"refmap\": \"${mod_id}.refmap.json\",\n  \"compatibilityLevel\": \"JAVA_18\",\n  \"mixins\": [\n    \"access.block.AccessorBannerBlockEntity\",\n    \"access.block.AccessorBaseContainerBlockEntity\",\n    \"access.block.AccessorCropBlock\",\n    \"access.entity.AccessorEntity\",\n    \"access.level.AccessorRecipeManager\",\n    \"access.loot.AccessorCompositeEntryBase\",\n    \"access.loot.AccessorDynamicLoot\",\n    \"access.loot.AccessorLootItem\",\n    \"access.loot.AccessorLootPool\",\n    \"access.loot.AccessorLootPoolSingletonContainer\",\n    \"access.loot.AccessorLootTable\",\n    \"access.loot.AccessorNestedLootTable\",\n    \"access.loot.AccessorTagEntry\",\n    \"access.particles.AccessSimpleParticleType\",\n    \"patch.advancement.MixinPlayerAdvancements\",\n    \"patch.block.MixinDecoratedPotPatterns\",\n    \"patch.entity.MixinLightningBolt\",\n    \"patch.entity.MixinLivingEntity\",\n    \"patch.item.MixinCreativeModeTab\",\n    \"patch.level.MixinRecipeManager\",\n    \"patch.level.MixinWalkNodeEvaluator\",\n    \"patch.loot.MixinLootDataType\",\n    \"patch.loot.MixinLootItemKilledByPlayerCondition\",\n    \"patch.loot.MixinLootPool\",\n    \"patch.packs.MixinSimpleJsonResourceReloadListener\",\n    \"patch.potions.MixinPotionBrewing\",\n    \"patch.server.MixinReloadableServerResources\"\n  ],\n  \"client\": [\n    \"access.block.AccessorBlockEntityRenderers\",\n    \"access.client.AccessorFontManager\",\n    \"access.client.AccessorItemBlockRenderTypes\",\n    \"access.client.AccessorMinecraft\",\n    \"access.client.gui.AccessorAbstractWidget\",\n    \"patch.client.MixinClientPacketListener\",\n    \"patch.locale.MixinClientLanguage\"\n  ],\n  \"server\": [],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/damage_type/fake_player.json",
    "content": "{\n  \"exhaustion\": 0.1,\n  \"message_id\": \"player\",\n  \"scaling\": \"when_caused_by_living_non_player\"\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/damage_type/fake_player.json",
    "content": "{\n  \"comments\": [\n    \"Damage types in this tag are considered fake player damage. Fake player  \",\n    \"damage acts like regular player damage but does not require an associated\",\n    \"player entity. This allows something like a block or mob to deal damage  \",\n    \"that causes mobs to drop exp and player only loot when killed.           \"\n  ],\n  \"values\": [\n    \"bookshelf:fake_player\"\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/building_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/colored_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/combat.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/food_and_drinks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/functional_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/ingredients.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/natural_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/op_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/redstone_blocks.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/spawn_eggs.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/tools_and_utilities.json",
    "content": "{\n  \"values\": [\n  ]\n}"
  },
  {
    "path": "common/src/main/resources/pack.mcmeta",
    "content": "{\n    \"pack\": {\n        \"description\": \"${mod_name}\",\n        \"pack_format\": 8\n    }\n}"
  },
  {
    "path": "fabric/build.gradle",
    "content": "plugins {\r\n    id 'multiloader-loader'\r\n    id 'fabric-loom'\r\n    id 'net.darkhax.curseforgegradle'\r\n    id 'com.modrinth.minotaur'\r\n}\r\n\r\nif (project.hasProperty('modmenu_version')) {\r\n    repositories { RepositoryHandler handler -> {\r\n        limitedMaven(handler, 'https://maven.terraformersmc.com/', 'com.terraformersmc')\r\n    }}\r\n\r\n    dependencies {\r\n        modRuntimeOnly(\"com.terraformersmc:modmenu:${project.findProperty('modmenu_version')}\")\r\n    }\r\n}\r\n\r\ndependencies {\r\n    minecraft \"com.mojang:minecraft:${minecraft_version}\"\r\n    mappings loom.layered {\r\n        officialMojangMappings()\r\n        parchment(\"org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip\")\r\n    }\r\n    modImplementation \"net.fabricmc:fabric-loader:${fabric_loader_version}\"\r\n    modImplementation \"net.fabricmc.fabric-api:fabric-api:${fabric_version}\"\r\n    if (project.hasProperty('jei_version')) {\r\n        modCompileOnlyApi(\"mezz.jei:jei-${minecraft_version}-common-api:${jei_version}\")\r\n        modCompileOnlyApi(\"mezz.jei:jei-${minecraft_version}-fabric-api:${jei_version}\")\r\n        modRuntimeOnly(\"mezz.jei:jei-${minecraft_version}-fabric:${jei_version}\")\r\n    }\r\n}\r\n\r\nloom {\r\n    mixin {\r\n        defaultRefmapName.set(\"${mod_id}.refmap.json\")\r\n    }\r\n    runs {\r\n        client {\r\n            client()\r\n            setConfigName('Fabric Client')\r\n            ideConfigGenerated(true)\r\n            runDir('runs/client')\r\n        }\r\n        server {\r\n            server()\r\n            setConfigName('Fabric Server')\r\n            ideConfigGenerated(true)\r\n            runDir('runs/server')\r\n        }\r\n    }\r\n}\r\n\r\n// CurseForge\r\ntask publishCurseForge(type: net.darkhax.curseforgegradle.TaskPublishCurseForge) {\r\n\r\n    apiToken = rootProject.findProperty('curse_auth')\r\n\r\n    var mainFile = upload(curse_project, tasks.remapJar)\r\n    mainFile.changelogType = 'markdown'\r\n    mainFile.changelog = rootProject.findProperty('mod_changelog')\r\n    mainFile.addJavaVersion('Java 21')\r\n    mainFile.addModLoader('Fabric')\r\n    mainFile.addModLoader('Quilt')\r\n    mainFile.releaseType = 'release'\r\n\r\n    if (rootProject.hasProperty('mod_client_only') && rootProject.findProperty('mod_client_only') == 'true') {\r\n        mainFile.addGameVersion('Client')\r\n    }\r\n    else {\r\n        mainFile.addGameVersion('Server', 'Client')\r\n    }\r\n\r\n    // Append Patreon Supporters\r\n    var patreonInfo = rootProject.findProperty('patreon')\r\n    if (patreonInfo) {\r\n        mainFile.changelog += \"\\n\\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\\n\\n${patreonInfo.pledgeLog}\"\r\n    }\r\n}\r\n\r\n// Modrinth\r\nmodrinth {\r\n\r\n    var patreonInfo = rootProject.findProperty('patreon')\r\n    var changelogText = rootProject.findProperty('mod_changelog')\r\n\r\n    if (patreonInfo) {\r\n        changelogText += \"\\n\\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\\n\\n${patreonInfo.pledgeLog}\"\r\n    }\r\n\r\n    token.set(rootProject.findProperty('modrinth_auth'))\r\n    projectId.set(modrinth_project)\r\n    changelog = changelogText\r\n    versionName.set(\"${mod_name}-fabric-${minecraft_version}-$version\")\r\n    versionType.set(\"release\")\r\n    loaders = [\"fabric\", \"quilt\"]\r\n    gameVersions = [\"${minecraft_version}\"]\r\n    uploadFile.set(tasks.remapJar)\r\n    dependencies {\r\n        required.project(\"fabric-api\")\r\n    }\r\n}\r\n\r\nvoid limitedMaven(RepositoryHandler handler, String url, String... groups) {\r\n    handler.exclusiveContent {\r\n        it.forRepositories(handler.maven {\r\n            setUrl(url)\r\n        })\r\n        it.filter { f ->\r\n            for (def group : groups) {\r\n                f.includeGroup(group)\r\n            }\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/FabricMod.java",
    "content": "package net.darkhax.bookshelf.fabric.impl;\n\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.BookshelfMod;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.fabric.impl.util.FabricRegistryHelper;\nimport net.fabricmc.api.ModInitializer;\nimport net.minecraft.DetectedVersion;\n\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.concurrent.CompletableFuture;\n\npublic class FabricMod implements ModInitializer {\n\n    @Override\n    public void onInitialize() {\n        BookshelfMod.getInstance().init();\n        Services.CONTENT.get().forEach(FabricRegistryHelper::new);\n        CompletableFuture.runAsync(FabricMod::checkForUpdates);\n    }\n\n    private static void checkForUpdates() {\n        try {\n            final HttpURLConnection connection = (HttpURLConnection) new URL(\"https://updates.blamejared.com/get?n=\" + Constants.MOD_ID + \"&gv=\" + DetectedVersion.BUILT_IN.getName() + \"&ml=fabric\").openConnection();\n            connection.setRequestMethod(\"HEAD\");\n            int responseCode = connection.getResponseCode();\n            if (responseCode != 200) {\n                Constants.LOG.warn(\"Version checker is not available.\");\n            }\n        }\n        catch (Exception e) {\n            // TODO\n        }\n    }\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/FabricModClient.java",
    "content": "package net.darkhax.bookshelf.fabric.impl;\n\nimport net.fabricmc.api.ClientModInitializer;\n\npublic class FabricModClient implements ClientModInitializer {\n    @Override\n    public void onInitializeClient() {\n    }\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/data/FabricIngredient.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.data;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient;\nimport net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.List;\nimport java.util.function.Supplier;\n\npublic class FabricIngredient<T extends IngredientLogic<T>> implements CustomIngredient {\n\n    private final T logic;\n    private final Supplier<CustomIngredientSerializer<?>> type;\n\n    public FabricIngredient(T logic, Supplier<CustomIngredientSerializer<?>> type) {\n        this.logic = logic;\n        this.type = type;\n    }\n\n    @Override\n    public boolean test(ItemStack stack) {\n        return this.logic.test(stack);\n    }\n\n    @Override\n    public List<ItemStack> getMatchingStacks() {\n        return this.logic.getAllMatchingStacks();\n    }\n\n    @Override\n    public boolean requiresTesting() {\n        return this.logic.requiresTesting();\n    }\n\n    @Override\n    public CustomIngredientSerializer<?> getSerializer() {\n        return this.type.get();\n    }\n\n    public static <T extends IngredientLogic<T>> CustomIngredientSerializer<FabricIngredient<T>> make(ResourceLocation id, MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> stream) {\n        final Supplier<CustomIngredientSerializer<?>> typeLookup = () -> CustomIngredientSerializer.get(id);\n        final MapCodec<FabricIngredient<T>> ingredientCodec = codec.xmap(l -> new FabricIngredient<>(l, typeLookup), i -> i.logic);\n        final StreamCodec<RegistryFriendlyByteBuf, FabricIngredient<T>> ingredientStream = stream.map(l -> new FabricIngredient<>(l, typeLookup), i -> i.logic);\n\n        return new CustomIngredientSerializer<>() {\n            @Override\n            public ResourceLocation getIdentifier() {\n                return id;\n            }\n\n            @Override\n            public MapCodec<FabricIngredient<T>> getCodec(boolean allowEmpty) {\n                return ingredientCodec;\n            }\n\n            @Override\n            public StreamCodec<RegistryFriendlyByteBuf, FabricIngredient<T>> getPacketCodec() {\n                return ingredientStream;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/network/FabricNetworkHandler.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.network;\n\nimport net.darkhax.bookshelf.common.api.network.INetworkHandler;\nimport net.darkhax.bookshelf.common.api.network.IPacket;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerPlayer;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class FabricNetworkHandler implements INetworkHandler {\n\n    private static final Map<ResourceLocation, IPacket<?>> PACKETS = new HashMap<>();\n\n    @Override\n    public <T extends CustomPacketPayload> void register(IPacket<T> packet) {\n        PayloadTypeRegistry.playC2S().register(packet.type(), packet.streamCodec());\n        PayloadTypeRegistry.playS2C().register(packet.type(), packet.streamCodec());\n        if (Services.PLATFORM.isPhysicalClient() && packet.destination().handledByClient()) {\n            ClientPlayNetworking.registerGlobalReceiver(packet.type(), (payload, context) -> {\n                context.client().execute(() -> {\n                    packet.handle(null, false, payload);\n                });\n            });\n        }\n        if (packet.destination().handledByServer()) {\n            ServerPlayNetworking.registerGlobalReceiver(packet.type(), (payload, context) -> {\n                context.server().execute(() -> {\n                    packet.handle(context.player(), true, payload);\n                });\n            });\n        }\n        PACKETS.put(packet.type().id(), packet);\n    }\n\n    @Override\n    public <T extends CustomPacketPayload> void sendToServer(T payload) {\n        final ResourceLocation id = payload.type().id();\n        if (!PACKETS.containsKey(id)) {\n            Constants.LOG.error(\"Attempted to send unregistered packet {} to the server.\", id);\n            throw new IllegalStateException(\"Attempted to send unregistered packet \" + id + \" to the server.\");\n        }\n        if (Minecraft.getInstance().player == null) {\n            Constants.LOG.error(\"Attempted to send packet {} to the server before a player instance is available.\", id);\n            throw new IllegalStateException(\"Attempted to send packet \" + id + \" to the server before a player instance is available.\");\n        }\n        ClientPlayNetworking.send(payload);\n    }\n\n    @Override\n    public <T extends CustomPacketPayload> void sendToPlayer(ServerPlayer recipient, T payload) {\n        final ResourceLocation id = payload.type().id();\n        if (!PACKETS.containsKey(id)) {\n            Constants.LOG.error(\"Attempted to send unregistered packet {} to player {}.\", id, recipient);\n            throw new IllegalStateException(\"Attempted to send unregistered packet \" + id + \" to player \" + recipient);\n        }\n        ServerPlayNetworking.send(recipient, payload);\n    }\n\n    @Override\n    public boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId) {\n        return ServerPlayNetworking.canSend(recipient, payloadId);\n    }\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricGameplayHelper.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.util;\n\nimport net.darkhax.bookshelf.common.api.util.IGameplayHelper;\nimport net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup;\nimport net.fabricmc.fabric.api.transfer.v1.item.ItemStorage;\nimport net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;\nimport net.fabricmc.fabric.api.transfer.v1.storage.Storage;\nimport net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.world.level.block.state.BlockState;\n\nimport java.util.function.BiFunction;\n\npublic class FabricGameplayHelper implements IGameplayHelper {\n\n    @Override\n    public ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) {\n        final int initialCount = stack.getCount();\n        final ItemStack result = IGameplayHelper.super.inventoryInsert(level, pos, side, stack);\n        if (result.isEmpty() || result.getCount() != initialCount) {\n            return result;\n        }\n        final Storage<ItemVariant> storage = ItemStorage.SIDED.find(level, pos, side);\n        if (storage != null && storage.supportsInsertion()) {\n            try (Transaction tx = Transaction.openOuter()) {\n                final long count = storage.insert(ItemVariant.of(stack), stack.getCount(), tx);\n                tx.commit();\n                if (count >= stack.getCount()) {\n                    return ItemStack.EMPTY;\n                }\n                else {\n                    final ItemStack txResult = stack.copy();\n                    txResult.shrink((int) count);\n                    return txResult;\n                }\n            }\n        }\n        return stack;\n    }\n\n    @Override\n    public <T extends BlockEntity> BlockEntityType.Builder<T> blockEntityBuilder(BiFunction<BlockPos, BlockState, T> factory, Block... validBlocks) {\n        BlockEntityType.BlockEntitySupplier<T> supplier = factory::apply;\n        return BlockEntityType.Builder.of(supplier, validBlocks);\n    }\n\n    @Override\n    public CreativeModeTab.Builder tabBuilder() {\n        return FabricItemGroup.builder();\n    }\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricPlatformHelper.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.util;\n\nimport net.darkhax.bookshelf.common.api.ModEntry;\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.util.IPlatformHelper;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.fabric.impl.gametest.FabricGameTestHelper;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.fabricmc.loader.api.metadata.ModMetadata;\n\nimport java.nio.file.Path;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class FabricPlatformHelper implements IPlatformHelper {\n\n    private static final Supplier<Set<ModEntry>> LOADED_MODS = CachedSupplier.cache(() -> FabricLoader.getInstance().getAllMods().stream().map(mod -> {\n        final ModMetadata meta = mod.getMetadata();\n        return new ModEntry(meta.getId(), meta.getName(), meta.getDescription(), meta.getVersion().getFriendlyString());\n    }).collect(Collectors.toSet()));\n\n    @Override\n    public Path getGamePath() {\n        return FabricLoader.getInstance().getGameDir();\n    }\n\n    @Override\n    public Path getConfigPath() {\n        return FabricLoader.getInstance().getConfigDir();\n    }\n\n    @Override\n    public Path getModsPath() {\n        return this.getGamePath().resolve(\"mods\");\n    }\n\n    @Override\n    public boolean isModLoaded(String modId) {\n        return FabricLoader.getInstance().isModLoaded(modId);\n    }\n\n    @Override\n    public boolean isDevelopmentEnvironment() {\n        return FabricLoader.getInstance().isDevelopmentEnvironment();\n    }\n\n    @Override\n    public PhysicalSide getPhysicalSide() {\n        return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT ? PhysicalSide.CLIENT : PhysicalSide.SERVER;\n    }\n\n    @Override\n    public Set<ModEntry> getLoadedMods() {\n        return LOADED_MODS.get();\n    }\n\n    @Override\n    public boolean isTestingEnvironment() {\n        return FabricGameTestHelper.ENABLED;\n    }\n\n    @Override\n    public String getName() {\n        return \"Fabric\";\n    }\n}"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricRegistryHelper.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.util;\n\nimport com.google.common.collect.Multimap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.registry.ContentProvider;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter;\nimport net.darkhax.bookshelf.common.mixin.access.client.AccessorItemBlockRenderTypes;\nimport net.darkhax.bookshelf.fabric.impl.data.FabricIngredient;\nimport net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;\nimport net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;\nimport net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;\nimport net.minecraft.client.gui.screens.MenuScreens;\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.client.renderer.blockentity.BlockEntityRenderers;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.entity.npc.VillagerProfession;\nimport net.minecraft.world.entity.npc.VillagerTrades;\nimport net.minecraft.world.flag.FeatureFlags;\nimport net.minecraft.world.inventory.MenuType;\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic final class FabricRegistryHelper {\n\n    private final ContentProvider content;\n    private final RegistrationContext context;\n\n    public FabricRegistryHelper(ContentProvider content) {\n        this.content = content;\n        this.context = new RegistrationContext(content.namespace());\n        if (content.canLoad()) {\n            this.registerContent();\n            this.registerVillagerTrades();\n            this.registerCommands();\n            if (Services.PLATFORM.isPhysicalClient()) {\n                this.registerClient();\n            }\n        }\n        else {\n            Constants.LOG.debug(\"Content provider {} is disabled.\", content);\n        }\n    }\n\n    private void registerContent() {\n        this.content.defineLoadConditions(new GenericRegistryAdapter<>(this.context, (id, val) -> LoadConditions.register(id, val.get())));\n        this.content.defineBlocks(new BlockRegistryAdapter(this.context, Registries.BLOCK, adapt(BuiltInRegistries.BLOCK)));\n        this.context.getPlaceableBlocks().forEach((ref, factory) -> Registry.register(BuiltInRegistries.ITEM, ref.key().location(), factory.apply(ref.value().get())));\n        this.content.defineItems(new GameRegistryAdapter<>(this.context, Registries.ITEM, adapt(BuiltInRegistries.ITEM)));\n        this.content.defineCreativeTabs(new CreativeModeTabAdapter(this.context, Registries.CREATIVE_MODE_TAB, adapt(BuiltInRegistries.CREATIVE_MODE_TAB)));\n        this.content.defineIngredientTypes(new IngredientTypeAdapter(this.context, (id, value) -> CustomIngredientSerializer.register(adaptType(id, value.get()))));\n        this.content.defineRecipeTypes(new RecipeTypeAdapter(this.context, Registries.RECIPE_TYPE, adapt(BuiltInRegistries.RECIPE_TYPE)));\n        this.content.defineAttributes(new GameRegistryAdapter<>(this.context, Registries.ATTRIBUTE, adapt(BuiltInRegistries.ATTRIBUTE)));\n        this.content.defineMobEffects(new GameRegistryAdapter<>(this.context, Registries.MOB_EFFECT, adapt(BuiltInRegistries.MOB_EFFECT)));\n        this.content.defineCriteriaTriggers(new GameRegistryAdapter<>(this.context, Registries.TRIGGER_TYPE, adapt(BuiltInRegistries.TRIGGER_TYPES)));\n        this.content.defineItemSubPredicates(new GameRegistryAdapter<>(this.context, Registries.ITEM_SUB_PREDICATE_TYPE, adapt(BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE)));\n        this.content.defineEntities(new GameRegistryAdapter<>(this.context, Registries.ENTITY_TYPE, adapt(BuiltInRegistries.ENTITY_TYPE)));\n        this.content.defineCatVariants(new GameRegistryAdapter<>(this.context, Registries.CAT_VARIANT, adapt(BuiltInRegistries.CAT_VARIANT)));\n        this.content.definePotions(new GameRegistryAdapter<>(this.context, Registries.POTION, adapt(BuiltInRegistries.POTION)));\n        this.content.definePotPatterns(new PotPatternAdapter(this.context, Registries.DECORATED_POT_PATTERN, adapt(BuiltInRegistries.DECORATED_POT_PATTERN)));\n        this.content.defineItemComponents(new GameRegistryAdapter<>(this.context, Registries.DATA_COMPONENT_TYPE, adapt(BuiltInRegistries.DATA_COMPONENT_TYPE)));\n        this.content.defineEnchantmentComponents(new GameRegistryAdapter<>(this.context, Registries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, adapt(BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE)));\n        this.content.defineLootConditions(new GameRegistryAdapter<>(this.context, Registries.LOOT_CONDITION_TYPE, adapt(BuiltInRegistries.LOOT_CONDITION_TYPE)));\n        this.content.defineLootFunctions(new GameRegistryAdapter<>(this.context, Registries.LOOT_FUNCTION_TYPE, adapt(BuiltInRegistries.LOOT_FUNCTION_TYPE)));\n        this.content.defineBlockEntities(new GameRegistryAdapter<>(this.context, Registries.BLOCK_ENTITY_TYPE, adapt(BuiltInRegistries.BLOCK_ENTITY_TYPE)));\n        this.content.defineRecipeSerializers(new GameRegistryAdapter<>(this.context, Registries.RECIPE_SERIALIZER, adapt(BuiltInRegistries.RECIPE_SERIALIZER)));\n        this.content.defineLootEntryTypes(new LootEntryTypeAdapter(this.context, Registries.LOOT_POOL_ENTRY_TYPE, adapt(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE)));\n        this.content.defineMenuType(new MenuTypeAdapter(this.context, (key, factory) -> Registry.register(BuiltInRegistries.MENU, key, new MenuType<>(factory.get()::create, FeatureFlags.VANILLA_SET))));\n        this.content.definePackets(new PacketAdapter(this.context, Services.NETWORK::register));\n        this.content.defineSounds(new SoundEventAdapter(this.context, Registries.SOUND_EVENT, adapt(BuiltInRegistries.SOUND_EVENT)));\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private void registerClient() {\n        this.content.defineMenuScreens(new MenuScreenAdapter((id, factory) -> MenuScreens.register(id, (MenuScreens.ScreenConstructor) factory::create)));\n        final Map<Block, RenderType> blockRenderTypes = AccessorItemBlockRenderTypes.bookshelf$getBlockTypes();\n        this.content.defineBlockRenderTypes(new BlockRenderTypeAdapter(blockRenderTypes::put));\n        this.content.defineBlockRenderers(new BlockEntityRendererAdapter(BlockEntityRenderers::register));\n    }\n\n    private void registerVillagerTrades() {\n        final VillagerTradeAdapter register = new VillagerTradeAdapter();\n        this.content.defineTrades(register);\n        for (Map.Entry<VillagerProfession, Multimap<Integer, VillagerTrades.ItemListing>> professionData : register.getVillagerTrades().entrySet()) {\n            final Int2ObjectMap<VillagerTrades.ItemListing[]> professionTrades = VillagerTrades.TRADES.computeIfAbsent(professionData.getKey(), profession -> new Int2ObjectOpenHashMap<>());\n            for (int merchantTier : professionData.getValue().keySet()) {\n                final List<VillagerTrades.ItemListing> tradesForTier = new ArrayList<>(Arrays.asList(professionTrades.getOrDefault(merchantTier, new VillagerTrades.ItemListing[0])));\n                tradesForTier.addAll(professionData.getValue().get(merchantTier));\n                professionTrades.put(merchantTier, tradesForTier.toArray(new VillagerTrades.ItemListing[0]));\n            }\n        }\n        final List<VillagerTrades.ItemListing> commonTrades = register.getCommonWanderingTrades();\n        if (!commonTrades.isEmpty()) {\n            final List<VillagerTrades.ItemListing> tradeData = new ArrayList<>(Arrays.asList(VillagerTrades.WANDERING_TRADER_TRADES.get(1)));\n            tradeData.addAll(commonTrades);\n            VillagerTrades.WANDERING_TRADER_TRADES.put(1, tradeData.toArray(new VillagerTrades.ItemListing[0]));\n        }\n        final List<VillagerTrades.ItemListing> rareTrades = register.getRareWanderingTrades();\n        if (!rareTrades.isEmpty()) {\n            final List<VillagerTrades.ItemListing> tradeData = new ArrayList<>(Arrays.asList(VillagerTrades.WANDERING_TRADER_TRADES.get(2)));\n            tradeData.addAll(rareTrades);\n            VillagerTrades.WANDERING_TRADER_TRADES.put(2, tradeData.toArray(new VillagerTrades.ItemListing[0]));\n        }\n    }\n\n    private void registerCommands() {\n        CommandRegistrationCallback.EVENT.register(this.content::defineCommands);\n        this.content.defineCommandArguments(new CommandArgumentAdapter(this.context, (rl, info) -> registerCommandArgument(rl, info.get())));\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static void registerCommandArgument(ResourceLocation key, CommandArgumentAdapter.TypeInfo type) {\n        ArgumentTypeRegistry.registerArgumentType(key, type.argType(), type.typeIfo());\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static CustomIngredientSerializer adaptType(ResourceLocation id, IngredientTypeAdapter.IngredientType type) {\n        return FabricIngredient.make(id, type.codec(), type.stream());\n    }\n\n    private static <T> BiConsumer<ResourceKey<T>, Supplier<T>> adapt(Registry<T> registry) {\n        return (key, value) -> Registry.register(registry, key, value.get());\n    }\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricRenderHelper.java",
    "content": "package net.darkhax.bookshelf.fabric.impl.util;\n\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.darkhax.bookshelf.common.api.util.IRenderHelper;\nimport net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandler;\nimport net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.material.FluidState;\n\npublic class FabricRenderHelper implements IRenderHelper {\n\n    @Override\n    public void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay) {\n        final FluidRenderHandler renderer = FluidRenderHandlerRegistry.INSTANCE.get(fluidState.getType());\n        if (renderer != null) {\n            final int[] color = unpackARGB(renderer.getFluidColor(level, pos, fluidState));\n            // Correct Fabric API not supporting alpha.\n            if (color[0] == 0) {\n                color[0] = 255;\n            }\n            final TextureAtlasSprite sprite = renderer.getFluidSprites(level, pos, fluidState)[0];\n            renderBox(bufferSource.getBuffer(RenderType.translucent()), pose, sprite, light, overlay, color);\n        }\n    }\n}"
  },
  {
    "path": "fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.network.INetworkHandler",
    "content": "net.darkhax.bookshelf.fabric.impl.network.FabricNetworkHandler"
  },
  {
    "path": "fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IGameplayHelper",
    "content": "net.darkhax.bookshelf.fabric.impl.util.FabricGameplayHelper"
  },
  {
    "path": "fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IPlatformHelper",
    "content": "net.darkhax.bookshelf.fabric.impl.util.FabricPlatformHelper"
  },
  {
    "path": "fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IRenderHelper",
    "content": "net.darkhax.bookshelf.fabric.impl.util.FabricRenderHelper"
  },
  {
    "path": "fabric/src/main/resources/bookshelf.fabric.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8\",\n  \"package\": \"net.darkhax.bookshelf.fabric.mixin\",\n  \"refmap\": \"${mod_id}.refmap.json\",\n  \"compatibilityLevel\": \"JAVA_21\",\n  \"mixins\": [],\n  \"client\": [],\n  \"server\": [],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}"
  },
  {
    "path": "fabric/src/main/resources/fabric.mod.json",
    "content": "{\n    \"schemaVersion\": 1,\n    \"id\": \"${mod_id}\",\n    \"version\": \"${version}\",\n    \"name\": \"${mod_name}\",\n    \"description\": \"${mod_description}\",\n    \"authors\": [\n        \"${mod_author}\"\n    ],\n    \"contributors\": [\n        \"This project is made possible with Patreon support from players like you. Thank you!\",\n        \"${patreon_pledges}\"\n    ],\n    \"contact\": {\n        \"sources\": \"${mod_repo}\",\n        \"issues\": \"${mod_repo}/issues\",\n        \"homepage\": \"${curse_page}\"\n    },\n    \"license\": \"${mod_license}\",\n    \"icon\": \"logo_${mod_id}.png\",\n    \"environment\": \"${mod_target_environment}\",\n    \"entrypoints\": {\n        \"main\": [\n          \"net.darkhax.bookshelf.fabric.impl.FabricMod\"\n        ],\n        \"client\": [\n            \"net.darkhax.bookshelf.fabric.impl.FabricModClient\"\n        ]\n    },\n    \"mixins\": [\n        \"${mod_id}.mixins.json\",\n        \"${mod_id}.fabric.mixins.json\"\n    ],\n    \"depends\": {\n        \"fabricloader\": \">=${fabric_loader_version}\",\n        \"fabric-api\": \"*\",\n        \"minecraft\": \"${minecraft_version}\",\n        \"java\": \">=${java_version}\"\n    },\n    \"custom\": {\n        \"modmenu\": {\n            \"links\": {\n                \"modmenu.curseforge\": \"${curse_page}\",\n                \"modmenu.modrinth\": \"${modrinth_page}\",\n                \"modmenu.patreon\": \"${patreon_url}?${mod_id}\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project\nversion=21.1\ngroup=net.darkhax.bookshelf\njava_version=21\n\n# Mod\nmod_name=Bookshelf\nmod_author=Darkhax\nmod_id=bookshelf\nmod_license=LGPL 2.1\nmod_description=Bookshelf is a library mod that provides code, frameworks, and utilities for other mods. Many mods make use of Bookshelf and are powered by its code.\nmod_repo=https://github.com/Darkhax-Minecraft/Bookshelf\nmod_docs=https://docs.darkhax.net/mods/bookshelf\nmod_item_icon=minecraft:bookshelf\n\n# Build Flags\nmod_client_only=false\n\n# Common\nminecraft_version=1.21.1\nminecraft_version_range=[1.21.1, 1.22)\nneo_form_version=1.21.1-20240808.144430\nparchment_minecraft=1.21\nparchment_version=2024.07.28\njei_version=19.18.10.218\ncrt_version=21.0.3\n\n# NeoForge\nneoforge_version=21.1.209\nneoforge_loader_version_range=[4,)\n\n# Fabric\nfabric_version=0.116.6+1.21.1\nfabric_loader_version=0.17.2\nmodmenu_version=11.0.2\n\n# CurseForge\ncurse_project=228525\ncurse_page=https://www.curseforge.com/minecraft/mc-mods/bookshelf\n\n# Modrinth\nmodrinth_project=uy4Cnpcm\nmodrinth_page=https://modrinth.com/mod/bookshelf-lib\n\n# Gradle\norg.gradle.jvmargs=-Xmx3G\norg.gradle.daemon=false"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd \"${APP_HOME:-./}\" > /dev/null && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "neoforge/build.gradle",
    "content": "plugins {\r\n    id 'multiloader-loader'\r\n    id 'net.neoforged.moddev'\r\n    id 'net.darkhax.curseforgegradle'\r\n    id 'com.modrinth.minotaur'\r\n}\r\n\r\nneoForge {\r\n    version = neoforge_version\r\n    parchment {\r\n        minecraftVersion = parchment_minecraft\r\n        mappingsVersion = parchment_version\r\n    }\r\n    runs {\r\n        configureEach {\r\n            systemProperty('neoforge.enabledGameTestNamespaces', mod_id)\r\n            ideName = \"NeoForge ${it.name.capitalize()} (${project.path})\"\r\n        }\r\n        client {\r\n            client()\r\n        }\r\n        server {\r\n            server()\r\n        }\r\n    }\r\n    mods {\r\n        \"${mod_id}\" {\r\n            sourceSet sourceSets.main\r\n        }\r\n    }\r\n}\r\n\r\ndependencies {\r\n\r\n    if (project.hasProperty('jei_version')) {\r\n        compileOnly(\"mezz.jei:jei-${minecraft_version}-common-api:${jei_version}\")\r\n        compileOnly(\"mezz.jei:jei-${minecraft_version}-neoforge-api:${jei_version}\")\r\n        runtimeOnly(\"mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}\")\r\n    }\r\n}\r\n\r\n// CurseForge\r\ntask publishCurseForge(type: net.darkhax.curseforgegradle.TaskPublishCurseForge) {\r\n\r\n    apiToken = rootProject.findProperty('curse_auth')\r\n\r\n    var mainFile = upload(curse_project, jar)\r\n    mainFile.changelogType = 'markdown'\r\n    mainFile.changelog = rootProject.findProperty('mod_changelog')\r\n    mainFile.addJavaVersion('Java 21')\r\n    mainFile.releaseType = 'release'\r\n    mainFile.addModLoader('NeoForge')\r\n\r\n    if (rootProject.hasProperty('mod_client_only') && rootProject.findProperty('mod_client_only') == 'true') {\r\n        mainFile.addGameVersion('Client')\r\n    }\r\n    else {\r\n        mainFile.addGameVersion('Server', 'Client')\r\n    }\r\n\r\n    // Append Patreon Supporters\r\n    var patreonInfo = rootProject.findProperty('patreon')\r\n    if (patreonInfo) {\r\n        mainFile.changelog += \"\\n\\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\\n\\n${patreonInfo.pledgeLog}\"\r\n    }\r\n}\r\n\r\n// Modrinth\r\nmodrinth {\r\n\r\n    var patreonInfo = rootProject.findProperty('patreon')\r\n    var changelogText = rootProject.findProperty('mod_changelog')\r\n\r\n    if (patreonInfo) {\r\n        changelogText += \"\\n\\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\\n\\n${patreonInfo.pledgeLog}\"\r\n    }\r\n\r\n    token.set(rootProject.findProperty('modrinth_auth'))\r\n    projectId.set(modrinth_project)\r\n    changelog = changelogText\r\n    versionName.set(\"${mod_name}-neoforge-${minecraft_version}-${version}\")\r\n    versionType.set('release')\r\n    loaders = [\"neoforge\"]\r\n    gameVersions = [\"${minecraft_version}\"]\r\n    uploadFile.set(tasks.jar)\r\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/NeoForgeMod.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl;\n\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.BookshelfMod;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.neoforge.impl.network.NeoForgeNetworkHandler;\nimport net.darkhax.bookshelf.neoforge.impl.util.NeoForgeRegistryHelper;\nimport net.neoforged.bus.api.IEventBus;\nimport net.neoforged.fml.common.Mod;\n\n@Mod(Constants.MOD_ID)\npublic class NeoForgeMod {\n\n    public NeoForgeMod(IEventBus eventBus) {\n        BookshelfMod.getInstance().init();\n        Services.CONTENT.get().forEach(NeoForgeRegistryHelper::new);\n        if (Services.NETWORK instanceof NeoForgeNetworkHandler handler) {\n            eventBus.addListener(handler::registerPayloadHandlers);\n        }\n    }\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/data/NeoForgeIngredient.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.data;\n\nimport com.mojang.serialization.MapCodec;\nimport net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.item.ItemStack;\nimport net.neoforged.neoforge.common.crafting.ICustomIngredient;\nimport net.neoforged.neoforge.common.crafting.IngredientType;\nimport net.neoforged.neoforge.registries.NeoForgeRegistries;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic class NeoForgeIngredient<T extends IngredientLogic<T>> implements ICustomIngredient {\n\n    private final T logic;\n    private final Supplier<IngredientType<?>> type;\n\n    public NeoForgeIngredient(T logic, Supplier<IngredientType<?>> type) {\n        this.logic = logic;\n        this.type = type;\n    }\n\n    @Override\n    public boolean test(@NotNull ItemStack stack) {\n        return this.logic.test(stack);\n    }\n\n    @NotNull\n    @Override\n    public Stream<ItemStack> getItems() {\n        return this.logic.getAllMatchingStacks().stream();\n    }\n\n    @Override\n    public boolean isSimple() {\n        return !this.logic.requiresTesting();\n    }\n\n    @NotNull\n    @Override\n    public IngredientType<?> getType() {\n        return this.type.get();\n    }\n\n    public static <T extends IngredientLogic<T>> IngredientType<NeoForgeIngredient<T>> makeIngredientType(ResourceLocation id, MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> stream) {\n        final Supplier<IngredientType<?>> typeLookup = () -> NeoForgeRegistries.INGREDIENT_TYPES.get(id);\n        final MapCodec<NeoForgeIngredient<T>> ingredientCodec = codec.xmap(l -> new NeoForgeIngredient<>(l, typeLookup), i -> i.logic);\n        final StreamCodec<RegistryFriendlyByteBuf, NeoForgeIngredient<T>> ingredientStream = stream.map(l -> new NeoForgeIngredient<>(l, typeLookup), i -> i.logic);\n        return new IngredientType<>(ingredientCodec, ingredientStream);\n    }\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/network/NeoForgeNetworkHandler.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.network;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\nimport net.darkhax.bookshelf.common.api.network.INetworkHandler;\nimport net.darkhax.bookshelf.common.api.network.IPacket;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.neoforged.neoforge.network.PacketDistributor;\nimport net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;\nimport net.neoforged.neoforge.network.handling.IPayloadHandler;\nimport net.neoforged.neoforge.network.registration.PayloadRegistrar;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\npublic class NeoForgeNetworkHandler implements INetworkHandler {\n\n    private static final Map<ResourceLocation, IPacket<?>> PACKETS = new HashMap<>();\n    private static final Multimap<String, IPacket<?>> PACKETS_BY_NAMESPACE = HashMultimap.create();\n\n    @Override\n    public <T extends CustomPacketPayload> void register(IPacket<T> packet) {\n        PACKETS.put(packet.type().id(), packet);\n        PACKETS_BY_NAMESPACE.put(packet.type().id().getNamespace(), packet);\n    }\n\n    public void registerPayloadHandlers(RegisterPayloadHandlersEvent event) {\n        for (String namespace : PACKETS_BY_NAMESPACE.keySet()) {\n            final PayloadRegistrar registrar = event.registrar(namespace).optional();\n            for (IPacket packet : PACKETS_BY_NAMESPACE.get(namespace)) {\n                final IPayloadHandler handler = (payload, ctx) -> packet.handle(ctx.player() instanceof ServerPlayer serverPlayer ? serverPlayer : null, !ctx.player().level().isClientSide, payload);\n                switch (packet.destination()) {\n                    case SERVER_TO_CLIENT ->\n                            registrar.optional().commonToClient(packet.type(), packet.streamCodec(), handler);\n                    case CLIENT_TO_SERVER ->\n                            registrar.optional().commonToServer(packet.type(), packet.streamCodec(), handler);\n                    case BIDIRECTIONAL ->\n                            registrar.optional().commonBidirectional(packet.type(), packet.streamCodec(), handler);\n                }\n            }\n        }\n    }\n\n    @Override\n    public <T extends CustomPacketPayload> void sendToServer(T payload) {\n        final ResourceLocation id = payload.type().id();\n        if (!PACKETS.containsKey(id)) {\n            Constants.LOG.error(\"Attempted to send unregistered packet {} to the server.\", id);\n            throw new IllegalStateException(\"Attempted to send unregistered packet \" + id + \" to the server.\");\n        }\n        if (Minecraft.getInstance().player == null) {\n            Constants.LOG.error(\"Attempted to send packet {} to the server before a player instance is available.\", id);\n            throw new IllegalStateException(\"Attempted to send packet \" + id + \" to the server before a player instance is available.\");\n        }\n        Objects.requireNonNull(Minecraft.getInstance().getConnection()).getConnection().send(new ServerboundCustomPayloadPacket(payload));\n    }\n\n    @Override\n    public <T extends CustomPacketPayload> void sendToPlayer(ServerPlayer recipient, T payload) {\n        final ResourceLocation id = payload.type().id();\n        if (!PACKETS.containsKey(id)) {\n            Constants.LOG.error(\"Attempted to send unregistered packet {} to player {}.\", id, recipient);\n            throw new IllegalStateException(\"Attempted to send unregistered packet \" + id + \" to player \" + recipient);\n        }\n        PacketDistributor.sendToPlayer(recipient, payload);\n    }\n\n    @Override\n    public boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId) {\n        return recipient.connection.hasChannel(payloadId);\n    }\n}\n"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeGameplayHelper.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.util;\n\nimport net.darkhax.bookshelf.common.api.util.IGameplayHelper;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.neoforged.neoforge.capabilities.Capabilities;\nimport net.neoforged.neoforge.items.IItemHandler;\nimport net.neoforged.neoforge.items.ItemHandlerHelper;\n\nimport java.util.function.BiFunction;\n\npublic class NeoForgeGameplayHelper implements IGameplayHelper {\n\n    @Override\n    public ItemStack getCraftingRemainder(ItemStack input) {\n        final Item item = input.getItem();\n        return item.hasCraftingRemainingItem(input) ? item.getCraftingRemainingItem(input) : ItemStack.EMPTY;\n    }\n\n    @Override\n    public ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) {\n        final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, side);\n        return inventory != null ? ItemHandlerHelper.insertItemStacked(inventory, stack, false) : IGameplayHelper.super.inventoryInsert(level, pos, side, stack);\n    }\n\n    @Override\n    public <T extends BlockEntity> BlockEntityType.Builder<T> blockEntityBuilder(BiFunction<BlockPos, BlockState, T> factory, Block... validBlocks) {\n        BlockEntityType.BlockEntitySupplier<T> supplier = factory::apply;\n        return BlockEntityType.Builder.of(supplier, validBlocks);\n    }\n\n    @Override\n    public CreativeModeTab.Builder tabBuilder() {\n        return CreativeModeTab.builder();\n    }\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgePlatformHelper.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.util;\n\nimport net.darkhax.bookshelf.common.api.ModEntry;\nimport net.darkhax.bookshelf.common.api.PhysicalSide;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.util.IPlatformHelper;\nimport net.neoforged.fml.ModList;\nimport net.neoforged.fml.loading.FMLEnvironment;\nimport net.neoforged.fml.loading.FMLLoader;\nimport net.neoforged.fml.loading.FMLPaths;\nimport net.neoforged.neoforge.gametest.GameTestHooks;\n\nimport java.nio.file.Path;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class NeoForgePlatformHelper implements IPlatformHelper {\n\n    private static final Supplier<Set<ModEntry>> LOADED_MODS = CachedSupplier.cache(() -> ModList.get().getMods().stream().map(mod -> new ModEntry(mod.getModId(), mod.getDisplayName(), mod.getDescription(), mod.getVersion().toString())).collect(Collectors.toSet()));\n\n    @Override\n    public Path getGamePath() {\n        return FMLPaths.GAMEDIR.get();\n    }\n\n    @Override\n    public Path getConfigPath() {\n        return FMLPaths.CONFIGDIR.get();\n    }\n\n    @Override\n    public Path getModsPath() {\n        return FMLPaths.MODSDIR.get();\n    }\n\n    @Override\n    public boolean isModLoaded(String modId) {\n        return ModList.get().isLoaded(modId);\n    }\n\n    @Override\n    public boolean isDevelopmentEnvironment() {\n        return !FMLLoader.isProduction();\n    }\n\n    @Override\n    public PhysicalSide getPhysicalSide() {\n        return FMLEnvironment.dist.isClient() ? PhysicalSide.CLIENT : PhysicalSide.SERVER;\n    }\n\n    @Override\n    public Set<ModEntry> getLoadedMods() {\n        return LOADED_MODS.get();\n    }\n\n    @Override\n    public boolean isTestingEnvironment() {\n        return GameTestHooks.isGametestEnabled();\n    }\n\n    @Override\n    public String getName() {\n        return \"NeoForge\";\n    }\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeRegistryHelper.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.util;\n\nimport com.google.common.collect.Multimap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport net.darkhax.bookshelf.common.api.data.conditions.LoadConditions;\nimport net.darkhax.bookshelf.common.api.function.CachedSupplier;\nimport net.darkhax.bookshelf.common.api.registry.ContentProvider;\nimport net.darkhax.bookshelf.common.api.registry.RegistrationContext;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter;\nimport net.darkhax.bookshelf.common.api.service.Services;\nimport net.darkhax.bookshelf.common.impl.Constants;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter;\nimport net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter;\nimport net.darkhax.bookshelf.common.mixin.access.client.AccessorItemBlockRenderTypes;\nimport net.darkhax.bookshelf.neoforge.impl.data.NeoForgeIngredient;\nimport net.minecraft.client.gui.screens.MenuScreens;\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfo;\nimport net.minecraft.commands.synchronization.ArgumentTypeInfos;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.entity.npc.VillagerTrades;\nimport net.minecraft.world.flag.FeatureFlags;\nimport net.minecraft.world.inventory.MenuType;\nimport net.minecraft.world.level.block.Block;\nimport net.neoforged.bus.api.IEventBus;\nimport net.neoforged.fml.ModContainer;\nimport net.neoforged.fml.ModList;\nimport net.neoforged.fml.javafmlmod.FMLModContainer;\nimport net.neoforged.neoforge.client.event.EntityRenderersEvent;\nimport net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;\nimport net.neoforged.neoforge.common.NeoForge;\nimport net.neoforged.neoforge.common.crafting.IngredientType;\nimport net.neoforged.neoforge.event.RegisterCommandsEvent;\nimport net.neoforged.neoforge.event.village.VillagerTradesEvent;\nimport net.neoforged.neoforge.event.village.WandererTradesEvent;\nimport net.neoforged.neoforge.registries.NeoForgeRegistries;\nimport net.neoforged.neoforge.registries.RegisterEvent;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\npublic final class NeoForgeRegistryHelper {\n\n    private final ContentProvider content;\n    private final RegistrationContext context;\n    private final IEventBus modBus;\n\n    public NeoForgeRegistryHelper(ContentProvider content) {\n        this.content = content;\n        this.context = new RegistrationContext(content.namespace());\n        this.modBus = getModBus(content.namespace());\n        if (this.content.canLoad()) {\n            this.content.defineLoadConditions(new GenericRegistryAdapter<>(this.context, (id, val) -> LoadConditions.register(id, val.get())));\n            this.modBus.addListener(this::registerContent);\n            this.setupTradeRegistration();\n            this.setupCommandRegistration();\n            this.content.definePackets(new PacketAdapter(this.context, Services.NETWORK::register));\n            if (Services.PLATFORM.isPhysicalClient()) {\n                this.modBus.addListener(this::bindMenuScreens);\n                this.modBus.addListener(this::registerRenderers);\n            }\n        }\n        else {\n            Constants.LOG.debug(\"Content provider {} is disabled.\", content);\n        }\n    }\n\n    private void registerContent(RegisterEvent event) {\n        event.register(Registries.BLOCK, helper -> {\n            this.content.defineBlocks(new BlockRegistryAdapter(this.context, Registries.BLOCK, adapt(helper)));\n            if (Services.PLATFORM.isPhysicalClient()) {\n                final Map<Block, RenderType> blockRenderTypes = AccessorItemBlockRenderTypes.bookshelf$getBlockTypes();\n                this.content.defineBlockRenderTypes(new BlockRenderTypeAdapter(blockRenderTypes::put));\n            }\n        });\n        event.register(Registries.ITEM, helper -> {\n            this.context.getPlaceableBlocks().forEach((blockRef, builder) -> helper.register(blockRef.key().location(), builder.apply(blockRef.value().get())));\n            this.content.defineItems(new GameRegistryAdapter<>(this.context, Registries.ITEM, adapt(helper)));\n        });\n        this.adaptRegistry(event, Registries.CREATIVE_MODE_TAB, this.content::defineCreativeTabs, CreativeModeTabAdapter::new);\n        event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, helper -> this.content.defineIngredientTypes(new IngredientTypeAdapter(this.context, (id, value) -> helper.register(id, adaptType(id, value.get())))));\n        this.adaptRegistry(event, Registries.RECIPE_TYPE, this.content::defineRecipeTypes, RecipeTypeAdapter::new);\n        this.adaptRegistry(event, Registries.ATTRIBUTE, this.content::defineAttributes);\n        this.adaptRegistry(event, Registries.MOB_EFFECT, this.content::defineMobEffects);\n        this.adaptRegistry(event, Registries.TRIGGER_TYPE, this.content::defineCriteriaTriggers);\n        this.adaptRegistry(event, Registries.ITEM_SUB_PREDICATE_TYPE, this.content::defineItemSubPredicates);\n        this.adaptRegistry(event, Registries.ENTITY_TYPE, this.content::defineEntities);\n        this.adaptRegistry(event, Registries.CAT_VARIANT, this.content::defineCatVariants);\n        this.adaptRegistry(event, Registries.POTION, this.content::definePotions);\n        this.adaptRegistry(event, Registries.DECORATED_POT_PATTERN, this.content::definePotPatterns, PotPatternAdapter::new);\n        this.adaptRegistry(event, Registries.DATA_COMPONENT_TYPE, this.content::defineItemComponents);\n        this.adaptRegistry(event, Registries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, this.content::defineEnchantmentComponents);\n        this.adaptRegistry(event, Registries.LOOT_CONDITION_TYPE, this.content::defineLootConditions);\n        this.adaptRegistry(event, Registries.LOOT_FUNCTION_TYPE, this.content::defineLootFunctions);\n        this.adaptRegistry(event, Registries.BLOCK_ENTITY_TYPE, this.content::defineBlockEntities);\n        event.register(Registries.COMMAND_ARGUMENT_TYPE, helper -> this.content.defineCommandArguments(new CommandArgumentAdapter(this.context, (key, argType) -> registerCommandArgument(helper, key, argType.get()))));\n        this.adaptRegistry(event, Registries.RECIPE_SERIALIZER, this.content::defineRecipeSerializers);\n        this.adaptRegistry(event, Registries.LOOT_POOL_ENTRY_TYPE, this.content::defineLootEntryTypes, LootEntryTypeAdapter::new);\n        event.register(Registries.MENU, helper -> this.content.defineMenuType(new MenuTypeAdapter(this.context, (key, factory) -> helper.register(key, new MenuType<>(factory.get()::create, FeatureFlags.VANILLA_SET)))));\n        this.adaptRegistry(event, Registries.SOUND_EVENT, this.content::defineSounds, SoundEventAdapter::new);\n    }\n\n    private void setupCommandRegistration() {\n        NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> this.content.defineCommands(event.getDispatcher(), event.getBuildContext(), event.getCommandSelection()));\n    }\n\n    private void setupTradeRegistration() {\n        final CachedSupplier<VillagerTradeAdapter> trades = CachedSupplier.cache(() -> {\n            final VillagerTradeAdapter adapter = new VillagerTradeAdapter();\n            this.content.defineTrades(adapter);\n            return adapter;\n        });\n        NeoForge.EVENT_BUS.addListener(VillagerTradesEvent.class, event -> {\n            final Multimap<Integer, VillagerTrades.ItemListing> newTrades = trades.get().getVillagerTrades().get(event.getType());\n            if (newTrades != null && !newTrades.isEmpty()) {\n                final Int2ObjectMap<List<VillagerTrades.ItemListing>> tradeData = event.getTrades();\n                for (Map.Entry<Integer, VillagerTrades.ItemListing> entry : newTrades.entries()) {\n                    tradeData.computeIfAbsent(entry.getKey(), ArrayList::new).add(entry.getValue());\n                }\n            }\n        });\n        NeoForge.EVENT_BUS.addListener(WandererTradesEvent.class, event -> {\n            trades.get().getCommonWanderingTrades().forEach(event.getGenericTrades()::add);\n            trades.get().getRareWanderingTrades().forEach(event.getRareTrades()::add);\n        });\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private void bindMenuScreens(RegisterMenuScreensEvent event) {\n        final MenuScreenAdapter adapter = new MenuScreenAdapter((type, factory) -> event.register(type, (MenuScreens.ScreenConstructor) factory::create));\n        this.content.defineMenuScreens(adapter);\n    }\n\n    private void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {\n        this.content.defineBlockRenderers(new BlockEntityRendererAdapter(event::registerBlockEntityRenderer));\n    }\n\n    private <T> void adaptRegistry(RegisterEvent event, ResourceKey<Registry<T>> registry, Consumer<GameRegistryAdapter<T>> contentProvider) {\n        this.adaptRegistry(event, registry, contentProvider, (GameRegistryAdapterFactory<T, GameRegistryAdapter<T>>) GameRegistryAdapter::new);\n    }\n\n    private <T, A extends GameRegistryAdapter<T>> void adaptRegistry(RegisterEvent event, ResourceKey<Registry<T>> registry, Consumer<A> contentProvider, GameRegistryAdapterFactory<T, A> adapterFactory) {\n        event.register(registry, helper -> contentProvider.accept(adapterFactory.build(this.context, registry, adapt(helper))));\n    }\n\n    private static <T> BiConsumer<ResourceKey<T>, Supplier<T>> adapt(RegisterEvent.RegisterHelper<T> helper) {\n        return (key, value) -> helper.register(key, value.get());\n    }\n\n    private static <T> BiConsumer<ResourceLocation, Supplier<T>> adaptGeneric(RegisterEvent.RegisterHelper<T> helper) {\n        return (key, value) -> helper.register(key, value.get());\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static IngredientType adaptType(ResourceLocation id, IngredientTypeAdapter.IngredientType type) {\n        return NeoForgeIngredient.makeIngredientType(id, type.codec(), type.stream());\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private static void registerCommandArgument(RegisterEvent.RegisterHelper<ArgumentTypeInfo<?, ?>> helper, ResourceLocation key, CommandArgumentAdapter.TypeInfo type) {\n        helper.register(key, type.typeIfo());\n        ArgumentTypeInfos.registerByClass(type.argType(), type.typeIfo());\n    }\n\n    private static IEventBus getModBus(String modid) {\n        final ModContainer container = ModList.get().getModContainerById(modid).orElseThrow(() -> new IllegalArgumentException(\"Could not find mod '\" + modid + \"'.\"));\n        if (container instanceof FMLModContainer fmlContainer) {\n            final IEventBus modEventBus = fmlContainer.getEventBus();\n            if (modEventBus != null) {\n                return modEventBus;\n            }\n            throw new IllegalStateException(\"Mod '\" + modid + \"' does not have an event bus!\");\n        }\n        throw new IllegalStateException(\"Mod '\" + modid + \"' is not an FML mod!\");\n    }\n\n    @FunctionalInterface\n    public interface GameRegistryAdapterFactory<T, A extends GameRegistryAdapter<T>> {\n        A build(RegistrationContext context, ResourceKey<Registry<T>> registryKey, BiConsumer<ResourceKey<T>, Supplier<T>> registryFunc);\n    }\n}\n"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeRenderHelper.java",
    "content": "package net.darkhax.bookshelf.neoforge.impl.util;\n\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.darkhax.bookshelf.common.api.util.IRenderHelper;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.RenderType;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.resources.ResourceLocation;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.material.FluidState;\nimport net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions;\n\npublic class NeoForgeRenderHelper implements IRenderHelper {\n    @Override\n    public void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay) {\n        final IClientFluidTypeExtensions fluidType = IClientFluidTypeExtensions.of(fluidState);\n        final ResourceLocation texturePath = fluidType.getStillTexture();\n        final int[] color = unpackARGB(fluidType.getTintColor(fluidState, level, pos));\n        renderBox(bufferSource.getBuffer(RenderType.translucent()), pose, blockSprite(texturePath), light, overlay, color);\n    }\n}"
  },
  {
    "path": "neoforge/src/main/java/net/darkhax/bookshelf/neoforge/mixin/access/gui/screen/AccessorMenuScreens.java",
    "content": "package net.darkhax.bookshelf.neoforge.mixin.access.gui.screen;\n\nimport net.minecraft.client.gui.screens.MenuScreens;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.MenuAccess;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.MenuType;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(MenuScreens.class)\npublic interface AccessorMenuScreens {\n\n    @Invoker(\"register\")\n    static <M extends AbstractContainerMenu, U extends Screen & MenuAccess<M>> void register(MenuType<? extends M> type, MenuScreens.ScreenConstructor<M, U> factory) {\n        throw new IllegalStateException(\"Mixin failed to apply.\");\n    }\n}\n"
  },
  {
    "path": "neoforge/src/main/resources/META-INF/neoforge.mods.toml",
    "content": "modLoader = \"javafml\"\nloaderVersion = \"${neoforge_loader_version_range}\"\nlicense = \"${mod_license}\"\nissueTrackerURL=\"${mod_repo}/issues\"\n\n[[mods]]\nmodId = \"${mod_id}\"\nversion = \"${version}\"\ndisplayName = \"${mod_name}\"\nupdateJSONURL = \"https://updates.blamejared.com/get?n=${mod_id}&gv=${minecraft_version}&ml=${platform}\"\ndisplayURL = \"${curse_page}\"\nlogoFile=\"logo_${mod_id}.png\"\nlogoBlur = false\ncredits = \"This project is made possible with Patreon support from players like you. Thank you! ${patreon_pledges}\"\nauthors = \"${mod_author}\"\ndescription = \"${mod_description}\"\n\n[[mixins]]\nconfig = \"${mod_id}.mixins.json\"\n\n[[mixins]]\nconfig = \"${mod_id}.neoforge.mixins.json\"\n\n[[dependencies.${mod_id}]]\nmodId = \"neoforge\"\ntype = \"required\"\nversionRange = \"[${neoforge_version},)\"\nordering = \"NONE\"\nside = \"BOTH\"\n\n[[dependencies.${mod_id}]]\nmodId = \"minecraft\"\ntype = \"required\"\nversionRange = \"${minecraft_version_range}\"\nordering = \"NONE\"\nside = \"BOTH\""
  },
  {
    "path": "neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.network.INetworkHandler",
    "content": "net.darkhax.bookshelf.neoforge.impl.network.NeoForgeNetworkHandler"
  },
  {
    "path": "neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IGameplayHelper",
    "content": "net.darkhax.bookshelf.neoforge.impl.util.NeoForgeGameplayHelper"
  },
  {
    "path": "neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IPlatformHelper",
    "content": "net.darkhax.bookshelf.neoforge.impl.util.NeoForgePlatformHelper"
  },
  {
    "path": "neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IRenderHelper",
    "content": "net.darkhax.bookshelf.neoforge.impl.util.NeoForgeRenderHelper"
  },
  {
    "path": "neoforge/src/main/resources/bookshelf.neoforge.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8\",\n  \"package\": \"net.darkhax.bookshelf.neoforge.mixin\",\n  \"compatibilityLevel\": \"JAVA_21\",\n  \"mixins\": [\n  ],\n  \"client\": [\n    \"access.gui.screen.AccessorMenuScreens\"\n  ],\n  \"server\": [],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\r\n    repositories {\r\n        gradlePluginPortal()\r\n        mavenCentral()\r\n        exclusiveContent {\r\n            forRepository {\r\n                maven {\r\n                    name = 'Fabric'\r\n                    url = uri('https://maven.fabricmc.net')\r\n                }\r\n            }\r\n            filter {\r\n                includeGroupAndSubgroups('net.fabricmc')\r\n                includeGroup('fabric-loom')\r\n            }\r\n        }\r\n        exclusiveContent {\r\n            forRepository {\r\n                maven {\r\n                    name = 'Sponge Snapshots'\r\n                    url = uri(\"https://repo.spongepowered.org/repository/maven-public\")\r\n                }\r\n            }\r\n            filter {\r\n                includeGroupAndSubgroups(\"org.spongepowered\")\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\nplugins {\r\n    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'\r\n}\r\n\r\n// This should match the folder name of the project, or else IDEA may complain (see https://youtrack.jetbrains.com/issue/IDEA-317606)\r\nrootProject.name = 'Bookshelf'\r\ninclude('common')\r\ninclude('neoforge')\r\ninclude('fabric')"
  }
]