diff --git a/.commitlintrc b/.commitlintrc
new file mode 100644
index 0000000..05c12a0
--- /dev/null
+++ b/.commitlintrc
@@ -0,0 +1,6 @@
+{
+ "extends": ["@commitlint/config-conventional"],
+ "rules": {
+ "body-max-line-length": [1, "always", 200]
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f811f6a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Disable autocrlf on generated files, they always generate with LF
+# Add any extra files or paths here to make git stop saying they
+# are changed when only line endings change.
+src/generated/**/.cache/cache text eol=lf
+src/generated/**/*.json text eol=lf
diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml
new file mode 100644
index 0000000..5041885
--- /dev/null
+++ b/.github/workflows/pipeline.yaml
@@ -0,0 +1,44 @@
+name: Pipeline
+
+on:
+ push:
+ branches: [ "main" ]
+
+permissions:
+ contents: read # for checkout
+
+jobs:
+ pipeline:
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write # to be able to publish a GitHub release
+ issues: write # to be able to comment on released issues
+ pull-requests: write # to be able to comment on released pull requests
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'adopt'
+ java-version: '17'
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "lts/*"
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build & Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+ MODRINTH_PAT: ${{ secrets.MODRINTH_PAT }}
+ run: |
+ chmod +x ./build.sh
+ chmod +x ./gradlew
+ npx semantic-release@24
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ad71f2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,29 @@
+# eclipse
+bin
+*.launch
+.settings
+.metadata
+.classpath
+.project
+
+# idea
+out
+*.ipr
+*.iws
+*.iml
+.idea
+
+# gradle
+build
+.gradle
+
+# other
+eclipse
+run
+
+# Files from Forge MDK
+forge*changelog.txt
+
+# Semver
+node_modules/
+package-*.json
\ No newline at end of file
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 0000000..da99483
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+npx --no -- commitlint --edit "$1"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0c44706
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+# ⚙️ Create: Mob Spawners
+An addon for the [Create](https://github.com/Creators-of-Create/Create) mod for Minecraft. It adds a tool that can catch mobs which can then be placed into a spawner. It aims to be "immersive" by making use of the base game magic components.
+
+## 🆕 What does it add?
+### Soul Catcher
+
+![Soul Catcher](docs/soul_catcher.png)
+
+The Soul Catcher is a new item that can capture a mob's soul.
+For this to work, the mob needs to have an active weakness effect which could be applied using a throwable potion of weakness.
+Mobs that should not be capturable can be adjusted in the config. Bosses are not capturable by design.
+
+It can be crafted using this recipe:
+
+![Soul Catcher Recipe](docs/recipe_soul_catcher.png)
+
+This is how it looks ingame:
+
+![Soul Catcher Screencast](docs/soul_catcher_anim.webp)
+
+### Mechanical Spawner
+
+![Mechanical Spawner](docs/mechanical_spawner.png)
+
+The Mechanical Spawner is a new kinetic block that can spawn mobs according to the Soul Catcher placed inside.
+For this to work, it needs rotational force and a supply of potion of regeneration (as liquid pumped into the block).
+
+It can be crafted in a 5x5 Mechanical Crafter grid using this recipe:
+
+![Mechanical Spawner Recipe](docs/recipe_mechanical_spawner.png)
+
+This is how it looks ingame:
+
+![Mechanical Spawner Screencast](docs/mechanical_spawner_anim.webp)
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..19bb8de
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,233 @@
+plugins {
+ id 'eclipse'
+ id 'idea'
+ id 'maven-publish'
+ id 'net.minecraftforge.gradle' version '[6.0,6.2)'
+ id 'org.parchmentmc.librarian.forgegradle' version '1.+'
+}
+
+version = mod_version
+group = mod_group_id
+
+base {
+ archivesName = mod_id
+}
+
+// Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17.
+java.toolchain.languageVersion = JavaLanguageVersion.of(17)
+
+println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
+minecraft {
+ // The mappings can be changed at any time and must be in the following format.
+ // Channel: Version:
+ // official MCVersion Official field/method names from Mojang mapping files
+ // parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official
+ //
+ // You must be aware of the Mojang license when using the 'official' or 'parchment' mappings.
+ // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
+ //
+ // Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge
+ // Additional setup is needed to use their mappings: https://parchmentmc.org/docs/getting-started
+ //
+ // Use non-default mappings at your own risk. They may not always work.
+ // Simply re-run your setup task after changing the mappings to update your workspace.
+ mappings channel: mapping_channel, version: mapping_version
+
+ // When true, this property will have all Eclipse/IntelliJ IDEA run configurations run the "prepareX" task for the given run configuration before launching the game.
+ // In most cases, it is not necessary to enable.
+ // enableEclipsePrepareRuns = true
+ // enableIdeaPrepareRuns = true
+
+ // This property allows configuring Gradle's ProcessResources task(s) to run on IDE output locations before launching the game.
+ // It is REQUIRED to be set to true for this template to function.
+ // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html
+ copyIdeResources = true
+
+ // When true, this property will add the folder name of all declared run configurations to generated IDE run configurations.
+ // The folder name can be set on a run configuration using the "folderName" property.
+ // By default, the folder name of a run configuration is the name of the Gradle project containing it.
+ // generateRunFolders = true
+
+ // This property enables access transformers for use in development.
+ // They will be applied to the Minecraft artifact.
+ // The access transformer file can be anywhere in the project.
+ // However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge.
+ // This default location is a best practice to automatically put the file in the right place in the final jar.
+ // See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information.
+ // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
+
+ // Default run configurations.
+ // These can be tweaked, removed, or duplicated as needed.
+ runs {
+ // applies to all the run configs below
+ configureEach {
+ workingDirectory project.file('run')
+
+ // Recommended logging data for a userdev environment
+ // The markers can be added/remove as needed separated by commas.
+ // "SCAN": For mods scan.
+ // "REGISTRIES": For firing of registry events.
+ // "REGISTRYDUMP": For getting the contents of all registries.
+ property 'forge.logging.markers', 'REGISTRIES'
+
+ // Recommended logging level for the console
+ // You can set various levels here.
+ // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
+ property 'forge.logging.console.level', 'debug'
+
+ mods {
+ "${mod_id}" {
+ source sourceSets.main
+ }
+ }
+ }
+
+ client {
+ // Comma-separated list of namespaces to load gametests from. Empty = all namespaces.
+ property 'forge.enabledGameTestNamespaces', mod_id
+
+ property 'mixin.env.remapRefMap', 'true'
+ property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"
+ }
+
+ server {
+ property 'forge.enabledGameTestNamespaces', mod_id
+ args '--nogui'
+
+ property 'mixin.env.remapRefMap', 'true'
+ property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"
+ }
+
+ // This run config launches GameTestServer and runs all registered gametests, then exits.
+ // By default, the server will crash when no gametests are provided.
+ // The gametest system is also enabled by default for other run configs under the /test command.
+ gameTestServer {
+ property 'forge.enabledGameTestNamespaces', mod_id
+ }
+
+ data {
+ // example of overriding the workingDirectory set in configureEach above
+ workingDirectory project.file('run-data')
+
+ // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources.
+ args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
+ }
+ }
+}
+
+// Include resources generated by data generators.
+sourceSets.main.resources { srcDir 'src/generated/resources' }
+
+repositories {
+ // Put repositories for dependencies here
+ // ForgeGradle automatically adds the Forge maven and Maven Central for you
+
+ // If you have mod jar dependencies in ./libs, you can declare them as a repository like so.
+ // See https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:flat_dir_resolver
+ // flatDir {
+ // dir 'libs'
+ // }
+
+ // Create
+ maven {
+ name = 'tterrag maven'
+ url = 'https://maven.tterrag.com/'
+ }
+
+ // JEI
+ maven {
+ // location of the maven that hosts JEI files since January 2023
+ name = "Jared's maven"
+ url = "https://maven.blamejared.com/"
+ }
+
+ // Jade
+ maven {
+ url "https://www.cursemaven.com"
+ content {
+ includeGroup "curse.maven"
+ }
+ }
+}
+
+dependencies {
+ // Specify the version of Minecraft to use.
+ // Any artifact can be supplied so long as it has a "userdev" classifier artifact and is a compatible patcher artifact.
+ // The "userdev" classifier will be requested and setup by ForgeGradle.
+ // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"],
+ // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository.
+ minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
+
+ // Create
+ implementation fg.deobf("com.simibubi.create:create-${create_minecraft_version}:${create_version}:slim") { transitive = false }
+ implementation fg.deobf("com.jozufozu.flywheel:flywheel-forge-${flywheel_minecraft_version}:${flywheel_version}")
+ implementation fg.deobf("com.tterrag.registrate:Registrate:${registrate_version}")
+
+ // JEI
+ compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}")
+ compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}")
+ runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}")
+
+ // Jade
+ implementation fg.deobf("curse.maven:jade-324717:${jade_id}")
+}
+
+// This block of code expands all declared replace properties in the specified resource targets.
+// A missing property will result in an error. Properties are expanded using ${} Groovy notation.
+// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments.
+// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html
+tasks.named('processResources', ProcessResources).configure {
+ var replaceProperties = [
+ minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range,
+ forge_version: forge_version, forge_version_range: forge_version_range,
+ loader_version_range: loader_version_range,
+ mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version,
+ mod_authors: mod_authors, mod_description: mod_description,
+ ]
+ inputs.properties replaceProperties
+
+ filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) {
+ expand replaceProperties + [project: project]
+ }
+}
+
+// Example for how to get properties into the manifest for reading at runtime.
+tasks.named('jar', Jar).configure {
+ manifest {
+ attributes([
+ 'Specification-Title' : mod_id,
+ 'Specification-Vendor' : mod_authors,
+ 'Specification-Version' : '1', // We are version 1 of ourselves
+ 'Implementation-Title' : project.name,
+ 'Implementation-Version' : project.jar.archiveVersion,
+ 'Implementation-Vendor' : mod_authors,
+ 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
+ ])
+ }
+
+ // This is the preferred method to reobfuscate your jar file
+ finalizedBy 'reobfJar'
+}
+
+// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing:
+// tasks.named('publish').configure {
+// dependsOn 'reobfJar'
+// }
+
+// Example configuration to allow publishing using the maven-publish plugin
+publishing {
+ publications {
+ register('mavenJava', MavenPublication) {
+ artifact jar
+ }
+ }
+ repositories {
+ maven {
+ url "file://${project.projectDir}/mcmodsrepo"
+ }
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
+}
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..aa33a61
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,2 @@
+./gradlew reobfJar
+mv build/reobfJar/output.jar build/reobfJar/create-mob-spawners-1.20.1-"$1".jar
\ No newline at end of file
diff --git a/create_version_modrinth.js b/create_version_modrinth.js
new file mode 100644
index 0000000..21708ca
--- /dev/null
+++ b/create_version_modrinth.js
@@ -0,0 +1,94 @@
+const axios = require('axios').default;
+const FormData = require('form-data');
+const fs = require("node:fs");
+
+function escapeControlCharacters(str) {
+ return str.replace(/[\0-\x1F\x7F]/g, (char) => {
+ switch (char) {
+ case '\n':
+ return '\\n';
+ case '\r':
+ return '\\r';
+ case '\t':
+ return '\\t';
+ case '\b':
+ return '\\b';
+ case '\f':
+ return '\\f';
+ case '\v':
+ return '\\v';
+ case '\0':
+ return '\\0';
+ default:
+ return '\\x' + char.charCodeAt(0).toString(16).padStart(2, '0');
+ }
+ });
+}
+
+module.exports = {
+ verifyConditions: async (pluginConfig, context) => {
+ const {env} = context;
+ if (!env.MODRINTH_PAT.length) {
+ throw AggregateError('No Modrinth personal access token provided');
+ }
+ },
+ success: async (pluginConfig, context) => {
+ const {nextRelease} = context;
+ const version = nextRelease.version;
+ const changelog = escapeControlCharacters(nextRelease.notes);
+
+ const {env} = context;
+ const modrinthToken = env.MODRINTH_PAT;
+
+ const formData = new FormData();
+ formData.append('data', `{
+ "name": "Create: Mob Spawners ${version}",
+ "version_number": "${version}",
+ "changelog": "${changelog}",
+ "dependencies": [
+ {
+ "version_id": null,
+ "project_id": "LNytGWDc",
+ "file_name": null,
+ "dependency_type": "required"
+ },
+ {
+ "version_id": null,
+ "project_id": "nvQzSEkH",
+ "file_name": null,
+ "dependency_type": "optional"
+ },
+ {
+ "version_id": null,
+ "project_id": "u6dRKJwZ",
+ "file_name": null,
+ "dependency_type": "optional"
+ }
+ ],
+ "game_versions": [
+ "1.20.1"
+ ],
+ "loaders": [
+ "forge",
+ "neoforge"
+ ],
+ "version_type": "release",
+ "featured": true,
+ "status": "listed",
+ "project_id": "bklciXlt",
+ "file_parts": [
+ "file"
+ ]
+ }`);
+ formData.append('file', fs.createReadStream(`./build/reobfJar/create-mob-spawners-1.20.1-${version}.jar`));
+
+ let headers = formData.getHeaders();
+ headers.authorization = modrinthToken;
+
+ axios.post('https://api.modrinth.com/v2/version', formData, {
+ headers: headers,
+ }).then(result => {
+ console.log(result.data);
+ });
+ }
+}
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..f36624f
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,2 @@
+full_size/
+rendering/
\ No newline at end of file
diff --git a/docs/addon_icon.png b/docs/addon_icon.png
new file mode 100644
index 0000000..a77fdcd
Binary files /dev/null and b/docs/addon_icon.png differ
diff --git a/docs/mechanical_spawner.png b/docs/mechanical_spawner.png
new file mode 100644
index 0000000..2e8ee48
Binary files /dev/null and b/docs/mechanical_spawner.png differ
diff --git a/docs/mechanical_spawner_anim.webp b/docs/mechanical_spawner_anim.webp
new file mode 100644
index 0000000..2bcf540
Binary files /dev/null and b/docs/mechanical_spawner_anim.webp differ
diff --git a/docs/recipe_mechanical_spawner.png b/docs/recipe_mechanical_spawner.png
new file mode 100644
index 0000000..3ccc8e8
Binary files /dev/null and b/docs/recipe_mechanical_spawner.png differ
diff --git a/docs/recipe_soul_catcher.png b/docs/recipe_soul_catcher.png
new file mode 100644
index 0000000..43370a2
Binary files /dev/null and b/docs/recipe_soul_catcher.png differ
diff --git a/docs/soul_catcher.png b/docs/soul_catcher.png
new file mode 100644
index 0000000..96a4900
Binary files /dev/null and b/docs/soul_catcher.png differ
diff --git a/docs/soul_catcher_anim.webp b/docs/soul_catcher_anim.webp
new file mode 100644
index 0000000..18b7554
Binary files /dev/null and b/docs/soul_catcher_anim.webp differ
diff --git a/docs/title_image.png b/docs/title_image.png
new file mode 100644
index 0000000..f9d43dc
Binary files /dev/null and b/docs/title_image.png differ
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..19c35a2
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,73 @@
+# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
+# This is required to provide enough memory for the Minecraft decompilation process.
+org.gradle.jvmargs=-Xmx3G
+org.gradle.daemon=false
+
+
+## Environment Properties
+
+# The Minecraft version must agree with the Forge version to get a valid artifact
+minecraft_version=1.20.1
+# The Minecraft version range can use any release version of Minecraft as bounds.
+# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
+# as they do not follow standard versioning conventions.
+minecraft_version_range=[1.20.1,1.21)
+# The Forge version must agree with the Minecraft version to get a valid artifact
+forge_version=47.2.0
+# The Forge version range can use any version of Forge as bounds or match the loader version range
+forge_version_range=[47,)
+# The loader version range can only use the major version of Forge/FML as bounds
+loader_version_range=[47,)
+# The mapping channel to use for mappings.
+# The default set of supported mapping channels are ["official", "snapshot", "snapshot_nodoc", "stable", "stable_nodoc"].
+# Additional mapping channels can be registered through the "channelProviders" extension in a Gradle plugin.
+#
+# | Channel | Version | |
+# |-----------|----------------------|--------------------------------------------------------------------------------|
+# | official | MCVersion | Official field/method names from Mojang mapping files |
+# | parchment | YYYY.MM.DD-MCVersion | Open community-sourced parameter names and javadocs layered on top of official |
+#
+# You must be aware of the Mojang license when using the 'official' or 'parchment' mappings.
+# See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
+#
+# Parchment is an unofficial project maintained by ParchmentMC, separate from Minecraft Forge.
+# Additional setup is needed to use their mappings, see https://parchmentmc.org/docs/getting-started
+mapping_channel=parchment
+# The mapping version to query from the mapping channel.
+# This must match the format required by the mapping channel.
+mapping_version=2023.09.03-1.20.1
+
+
+## Mod Properties
+
+# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
+# Must match the String constant located in the main mod class annotated with @Mod.
+mod_id=create_mob_spawners
+# The human-readable display name for the mod.
+mod_name=Create: Mob Spawners
+# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
+mod_license=GNU General Public License v3.0
+# The mod version. See https://semver.org/
+mod_version=1.0.0
+# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
+# This should match the base package used for the mod sources.
+# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
+mod_group_id=dev.kvnmtz.createmobspawners
+# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
+mod_authors=3xpl01t
+# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
+mod_description=An addon for the Create mod which adds a way to capture and spawn mobs using magic.
+
+# Create
+create_minecraft_version = 1.20.1
+flywheel_minecraft_version = 1.20.1
+create_version = 0.5.1.j-55
+flywheel_version = 0.6.11-13
+registrate_version = MC1.20-1.3.3
+
+# JEI
+mc_version=1.20.1
+jei_version=15.20.0.105
+
+# Jade
+jade_id=5876199
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..943f0cb
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37aef8d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..65dcd68
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..93e3f59
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bc3124d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "create-mob-spawners",
+ "version": "1.0.0",
+ "author": "3xpl01t",
+ "private": "true",
+ "devDependencies": {
+ "@commitlint/cli": "^19.6.1",
+ "@commitlint/config-conventional": "^19.6.0",
+ "@semantic-release/exec": "^6.0.3",
+ "husky": "^9.1.7",
+ "axios": "^1.7.9",
+ "form-data": "^4.0.1"
+ },
+ "scripts": {
+ "prepare": "husky"
+ }
+}
diff --git a/release.config.cjs b/release.config.cjs
new file mode 100644
index 0000000..424d9a4
--- /dev/null
+++ b/release.config.cjs
@@ -0,0 +1,27 @@
+/**
+ * @type {import('semantic-release').GlobalConfig}
+ */
+module.exports = {
+ branches: ['main'],
+ plugins: [
+ '@semantic-release/commit-analyzer',
+ '@semantic-release/release-notes-generator',
+ [
+ '@semantic-release/exec',
+ {
+ prepareCmd: './build.sh ${nextRelease.version}',
+ },
+ ],
+ [
+ '@semantic-release/github',
+ {
+ 'assets': [
+ {
+ 'path': 'build/reobfJar/create-mob-spawners-1.20.1-*.jar',
+ },
+ ],
+ },
+ ],
+ './create_version_modrinth.js',
+ ],
+};
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b7fe2dc
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,14 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ maven {
+ name = 'MinecraftForge'
+ url = 'https://maven.minecraftforge.net/'
+ }
+ maven { url = 'https://maven.parchmentmc.org' }
+ }
+}
+
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
+}
\ No newline at end of file
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/Config.java b/src/main/java/dev/kvnmtz/createmobspawners/Config.java
new file mode 100644
index 0000000..0ebf1b0
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/Config.java
@@ -0,0 +1,37 @@
+package dev.kvnmtz.createmobspawners;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.EntityType;
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.event.config.ModConfigEvent;
+import net.minecraftforge.registries.ForgeRegistries;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Mod.EventBusSubscriber(modid = CreateMobSpawners.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
+public class Config {
+ private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
+
+ private static final ForgeConfigSpec.ConfigValue> SOUL_CATCHER_ENTITY_BLACKLIST = BUILDER
+ .comment("Entity ids that should not be catchable with the Soul Catcher")
+ .defineListAllowEmpty("soul_catcher_entity_blacklist", List.of("minecraft:iron_golem", "minecraft:snow_golem"), Config::validateEntityId);
+
+ static final ForgeConfigSpec SPEC = BUILDER.build();
+
+ public static Set> soulCatcherEntityBlacklist;
+
+ private static boolean validateEntityId(final Object obj) {
+ return obj instanceof final String entityId && ForgeRegistries.ENTITY_TYPES.containsKey(new ResourceLocation(entityId));
+ }
+
+ @SubscribeEvent
+ static void onLoad(final ModConfigEvent event) {
+ soulCatcherEntityBlacklist = SOUL_CATCHER_ENTITY_BLACKLIST.get().stream()
+ .map(entityId -> ForgeRegistries.ENTITY_TYPES.getValue(new ResourceLocation(entityId)))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/CreateMobSpawners.java b/src/main/java/dev/kvnmtz/createmobspawners/CreateMobSpawners.java
new file mode 100644
index 0000000..308e3ba
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/CreateMobSpawners.java
@@ -0,0 +1,94 @@
+package dev.kvnmtz.createmobspawners;
+
+import com.mojang.logging.LogUtils;
+import com.simibubi.create.compat.jei.ConversionRecipe;
+import com.simibubi.create.compat.jei.category.MysteriousItemConversionCategory;
+import com.simibubi.create.content.kinetics.BlockStressDefaults;
+import com.simibubi.create.foundation.item.ItemDescription;
+import com.simibubi.create.foundation.item.KineticStats;
+import com.simibubi.create.foundation.item.TooltipHelper;
+import com.simibubi.create.foundation.item.TooltipModifier;
+import dev.kvnmtz.createmobspawners.blocks.registry.ModBlocks;
+import dev.kvnmtz.createmobspawners.blocks.MechanicalSpawnerBlock;
+import dev.kvnmtz.createmobspawners.blocks.entity.renderer.MechanicalSpawnerBlockEntityRenderer;
+import dev.kvnmtz.createmobspawners.blocks.entity.registry.ModBlockEntities;
+import dev.kvnmtz.createmobspawners.items.ModCreativeModeTabs;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import dev.kvnmtz.createmobspawners.items.SoulCatcherItem;
+import dev.kvnmtz.createmobspawners.network.PacketHandler;
+import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.ModLoadingContext;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.config.ModConfig;
+import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
+import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import org.slf4j.Logger;
+
+import java.util.function.Function;
+
+@Mod(CreateMobSpawners.MOD_ID)
+public class CreateMobSpawners
+{
+ public static final String MOD_ID = "create_mob_spawners";
+ public static final Logger LOGGER = LogUtils.getLogger();
+
+ public CreateMobSpawners()
+ {
+ var modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
+
+ ModItems.register(modEventBus);
+ ModBlocks.register(modEventBus);
+ ModBlockEntities.register(modEventBus);
+ ModCreativeModeTabs.register(modEventBus);
+
+ modEventBus.addListener(this::commonSetup);
+
+ MinecraftForge.EVENT_BUS.register(this);
+
+ MinecraftForge.EVENT_BUS.register(SoulCatcherItem.class);
+ MinecraftForge.EVENT_BUS.register(MechanicalSpawnerBlock.class);
+
+ ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
+ }
+
+ public static ResourceLocation asResource(String path) {
+ return new ResourceLocation(MOD_ID, path);
+ }
+
+ private void commonSetup(final FMLCommonSetupEvent event)
+ {
+ event.enqueueWork(() -> {
+ PacketHandler.register();
+
+ BlockStressDefaults.DEFAULT_IMPACTS.put(asResource("mechanical_spawner"), 4.0);
+
+ Function- tooltipModifierFactory = item -> new ItemDescription.Modifier(item, TooltipHelper.Palette.PURPLE)
+ .andThen(TooltipModifier.mapNull(KineticStats.create(item)));
+ TooltipModifier.REGISTRY.registerDeferred(ModBlocks.SPAWNER.get().asItem(), tooltipModifierFactory);
+ TooltipModifier.REGISTRY.registerDeferred(ModItems.EMPTY_SOUL_CATCHER.get(), tooltipModifierFactory);
+ TooltipModifier.REGISTRY.registerDeferred(ModItems.SOUL_CATCHER.get(), tooltipModifierFactory);
+
+ try {
+ Class.forName("mezz.jei.api.JeiPlugin");
+ MysteriousItemConversionCategory.RECIPES.add(ConversionRecipe.create(ModItems.EMPTY_SOUL_CATCHER.get().getDefaultInstance(), ModItems.SOUL_CATCHER.get().getDefaultInstance()));
+ } catch (ClassNotFoundException ignored) {
+ }
+ });
+ }
+
+ @Mod.EventBusSubscriber(modid = MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
+ public static class ClientModEvents
+ {
+ @SubscribeEvent
+ public static void onClientSetup(FMLClientSetupEvent event)
+ {
+ BlockEntityRenderers.register(ModBlockEntities.SPAWNER_BE.get(), MechanicalSpawnerBlockEntityRenderer::new);
+ }
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/blocks/MechanicalSpawnerBlock.java b/src/main/java/dev/kvnmtz/createmobspawners/blocks/MechanicalSpawnerBlock.java
new file mode 100644
index 0000000..fa5202c
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/blocks/MechanicalSpawnerBlock.java
@@ -0,0 +1,122 @@
+package dev.kvnmtz.createmobspawners.blocks;
+
+import com.simibubi.create.content.kinetics.base.KineticBlock;
+import com.simibubi.create.foundation.block.IBE;
+import dev.kvnmtz.createmobspawners.blocks.entity.registry.ModBlockEntities;
+import dev.kvnmtz.createmobspawners.blocks.entity.MechanicalSpawnerBlockEntity;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import dev.kvnmtz.createmobspawners.items.SoulCatcherItem;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.context.BlockPlaceContext;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelReader;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.RenderShape;
+import net.minecraft.world.level.block.SoundType;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.world.level.block.state.properties.DirectionProperty;
+import net.minecraft.world.phys.BlockHitResult;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class MechanicalSpawnerBlock extends KineticBlock implements IBE {
+ public MechanicalSpawnerBlock() {
+ super(Properties.of().strength(10.f).sound(SoundType.METAL).noOcclusion());
+ registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));
+ }
+
+ public static final DirectionProperty FACING = DirectionProperty.create("facing", Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST);
+
+ @Override
+ protected void createBlockStateDefinition(StateDefinition.Builder pBuilder) {
+ pBuilder.add(FACING);
+ super.createBlockStateDefinition(pBuilder);
+ }
+
+ private MechanicalSpawnerBlockEntity getBlockEntity(Level level, BlockPos pos) {
+ return (MechanicalSpawnerBlockEntity) level.getBlockEntity(pos);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public @NotNull InteractionResult use(@NotNull BlockState pState, @NotNull Level pLevel, @NotNull BlockPos pPos, @NotNull Player pPlayer, @NotNull InteractionHand pHand, @NotNull BlockHitResult pHit) {
+ var itemStack = pPlayer.getMainHandItem();
+ var blockEntity = getBlockEntity(pLevel, pPos);
+ var hasStoredEntity = blockEntity.hasStoredEntity();
+ if (!hasStoredEntity && itemStack.getItem() == ModItems.SOUL_CATCHER.get()) {
+ var entityData = SoulCatcherItem.getEntityData(itemStack);
+ if (entityData.isEmpty()) return InteractionResult.FAIL;
+ blockEntity.setStoredEntityData(entityData.get());
+ itemStack.shrink(1);
+ } else if (hasStoredEntity && itemStack.isEmpty() && pPlayer.isShiftKeyDown()) {
+ blockEntity.ejectSoulCatcher();
+ } else {
+ return InteractionResult.PASS;
+ }
+
+ return InteractionResult.SUCCESS;
+ }
+
+ @Override
+ public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pMovedByPiston) {
+ if (pState.getBlock() != pNewState.getBlock()) {
+ var blockEntity = getBlockEntity(pLevel, pPos);
+ blockEntity.ejectSoulCatcher();
+ }
+
+ super.onRemove(pState, pLevel, pPos, pNewState, pMovedByPiston);
+ }
+
+ @Override
+ public Class getBlockEntityClass() {
+ return MechanicalSpawnerBlockEntity.class;
+ }
+
+ @Override
+ public BlockEntityType extends MechanicalSpawnerBlockEntity> getBlockEntityType() {
+ return ModBlockEntities.SPAWNER_BE.get();
+ }
+
+ @Override
+ public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
+ return new MechanicalSpawnerBlockEntity(blockPos, blockState);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public @NotNull RenderShape getRenderShape(@NotNull BlockState pState) {
+ return RenderShape.MODEL;
+ }
+
+ @Override
+ public Direction.Axis getRotationAxis(BlockState blockState) {
+ return Direction.Axis.Y;
+ }
+
+ @Override
+ public boolean hasShaftTowards(LevelReader world, BlockPos pos, BlockState state, Direction face) {
+ return face.getAxis() == Direction.Axis.Y;
+ }
+
+ private Direction getPlacementDirection(BlockPlaceContext context) {
+ var player = context.getPlayer();
+ var mirror = player != null && player.isShiftKeyDown();
+ if (mirror) {
+ return context.getHorizontalDirection();
+ } else {
+ return context.getHorizontalDirection().getOpposite();
+ }
+ }
+
+ @Override
+ public @Nullable BlockState getStateForPlacement(@NotNull BlockPlaceContext pContext) {
+ return this.defaultBlockState().setValue(FACING, getPlacementDirection(pContext));
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/MechanicalSpawnerBlockEntity.java b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/MechanicalSpawnerBlockEntity.java
new file mode 100644
index 0000000..257deb8
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/MechanicalSpawnerBlockEntity.java
@@ -0,0 +1,360 @@
+package dev.kvnmtz.createmobspawners.blocks.entity;
+
+import com.simibubi.create.content.kinetics.base.KineticBlockEntity;
+import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour;
+import com.simibubi.create.foundation.blockEntity.behaviour.fluid.SmartFluidTankBehaviour;
+import com.simibubi.create.foundation.utility.animation.LerpedFloat;
+import dev.kvnmtz.createmobspawners.blocks.MechanicalSpawnerBlock;
+import dev.kvnmtz.createmobspawners.blocks.entity.registry.ModBlockEntities;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.IEntityStorage;
+import dev.kvnmtz.createmobspawners.capabilities.registry.ModCapabilities;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.StoredEntityData;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import dev.kvnmtz.createmobspawners.network.ClientboundSpawnerEventPacket;
+import dev.kvnmtz.createmobspawners.network.PacketHandler;
+import dev.kvnmtz.createmobspawners.utils.DropUtils;
+import dev.kvnmtz.createmobspawners.utils.ParticleUtils;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.*;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.gameevent.GameEvent;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.ForgeCapabilities;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.fluids.FluidStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Optional;
+
+public class MechanicalSpawnerBlockEntity extends KineticBlockEntity implements IEntityStorage {
+ public MechanicalSpawnerBlockEntity(BlockPos pPos, BlockState pBlockState) {
+ super(ModBlockEntities.SPAWNER_BE.get(), pPos, pBlockState);
+ }
+
+ private StoredEntityData storedEntityData = StoredEntityData.empty();
+
+ @Override
+ public StoredEntityData getStoredEntityData() {
+ return storedEntityData;
+ }
+
+ @Override
+ public void setStoredEntityData(StoredEntityData entityData) {
+ storedEntityData = entityData;
+ }
+
+ private SmartFluidTankBehaviour tank;
+ private LerpedFloat fluidLevel;
+
+ public LerpedFloat getFluidLevel() {
+ return fluidLevel;
+ }
+
+ private float getFillState() {
+ return (float) tank.getPrimaryHandler().getFluidAmount() / (float) tank.getPrimaryHandler().getCapacity();
+ }
+
+ public FluidStack getFluidStack() {
+ return tank.getPrimaryHandler().getFluid();
+ }
+
+ @Override
+ protected void write(CompoundTag compound, boolean clientPacket) {
+ super.write(compound, clientPacket);
+ compound.put("EntityStorage", storedEntityData.serializeNBT());
+ }
+
+ @Override
+ protected void read(CompoundTag compound, boolean clientPacket) {
+ super.read(compound, clientPacket);
+ storedEntityData.deserializeNBT(compound.getCompound("EntityStorage"));
+
+ if (clientPacket) {
+ var fillState = this.getFillState();
+ if (fluidLevel == null) {
+ fluidLevel = LerpedFloat.linear().startWithValue(fillState);
+ }
+ fluidLevel.chase(fillState, 0.5f, LerpedFloat.Chaser.EXP);
+ }
+ }
+
+ @Override
+ public void addBehaviours(List behaviours) {
+ tank = SmartFluidTankBehaviour.single(this, 1000);
+ tank.getPrimaryHandler().setValidator(fluidStack -> {
+ var tag = fluidStack.getTag();
+ if (tag == null) return false;
+ var potionType = tag.getString("Potion");
+ return potionType.equals("minecraft:regeneration")
+ || potionType.equals("minecraft:strong_regeneration")
+ || potionType.equals("minecraft:long_regeneration");
+ });
+ tank.whenFluidUpdates(() -> {
+ if (!isVirtual()) return;
+ if (fluidLevel == null) {
+ fluidLevel = LerpedFloat.linear().startWithValue(this.getFillState());
+ }
+ fluidLevel.chase(this.getFillState(), 0.5f, LerpedFloat.Chaser.EXP);
+ });
+ behaviours.add(tank);
+ }
+
+ @Override
+ public @NotNull LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) {
+ if (cap == ForgeCapabilities.FLUID_HANDLER && side == getBlockState().getValue(MechanicalSpawnerBlock.FACING).getOpposite())
+ return tank.getCapability().cast();
+
+ return super.getCapability(cap, side);
+ }
+
+ public void ejectSoulCatcher() {
+ if (level == null) return;
+ if (storedEntityData.isEmpty()) return;
+ var itemStack = ModItems.SOUL_CATCHER.get().getDefaultInstance();
+ itemStack.getCapability(ModCapabilities.ENTITY_STORAGE).ifPresent(entityStorage -> {
+ entityStorage.setStoredEntityData(storedEntityData);
+ var droppedItems = DropUtils.dropItemStack(level, worldPosition.getX(), worldPosition.getY() + 1, worldPosition.getZ(), itemStack);
+ droppedItems.stream().findFirst().ifPresent(itemEntity -> itemEntity.setGlowingTag(true));
+ storedEntityData = StoredEntityData.empty();
+ });
+ }
+
+ private int getNecessaryFluidAmountForSpawning() {
+ var fluid = tank.getPrimaryHandler().getFluid();
+ var potionType = fluid.getTag().getString("Potion");
+ if (potionType.equals("minecraft:strong_regeneration") || potionType.equals("minecraft:long_regeneration")) {
+ return 100;
+ }
+
+ return 200;
+ }
+
+ private static final int SPAWN_RANGE = 4;
+ private static final int MAX_NEARBY_ENTITIES = 6;
+
+ private float spawnProgress = 0;
+
+ public int getSpawnProgressPercentage() {
+ return Math.min(Math.round(spawnProgress * 100), 100);
+ }
+
+ private void addProgressForTick() {
+ if (spawnProgress >= 1.f) return;
+
+ final var BASE_TICKS_FOR_SPAWNING = 1200; // 1 minute at 1 RPM -> 4s at 256 RPM
+ spawnProgress += (0.05f * Mth.abs(speed) + 0.95f) / (float) BASE_TICKS_FOR_SPAWNING;
+ }
+
+ private int delayTicks = -1;
+ private DelayReason delayReason;
+
+ private void delay(int ticks, DelayReason reason) {
+ delayTicks = ticks;
+ delayReason = reason;
+ }
+
+ public boolean isDelayed() {
+ return delayTicks >= 0;
+ }
+
+ public String getDelayReasonTranslationKey() {
+ return "create_mob_spawners.waila.spawner_delay_reason." + delayReason.name().toLowerCase();
+ }
+
+ private void useFluid() {
+ var fluid = tank.getPrimaryHandler().getFluid();
+ fluid.setAmount(fluid.getAmount() - getNecessaryFluidAmountForSpawning());
+ }
+
+ private Optional> getEntityTypeFromStoredEntityData() {
+ var optEntityTypeResourceLocation = storedEntityData.getEntityType();
+ if (optEntityTypeResourceLocation.isEmpty()) return Optional.empty();
+
+ var entityTypeResourceLocation = optEntityTypeResourceLocation.get();
+
+ var tag = new CompoundTag();
+ tag.putString("id", entityTypeResourceLocation.toString());
+
+ return EntityType.by(tag);
+ }
+
+ private abstract static class EntitySpawnResult {
+ private static class SuccessfulResult extends EntitySpawnResult {
+ private final Entity entity;
+
+ private SuccessfulResult(Entity entity) {
+ this.entity = entity;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+ }
+
+ private static class DelayResult extends EntitySpawnResult {
+ private final DelayReason reason;
+
+ private DelayResult(DelayReason reason) {
+ this.reason = reason;
+ }
+
+ public DelayReason getReason() {
+ return reason;
+ }
+ }
+ }
+
+ private enum DelayReason {
+ UNKNOWN(20),
+ INVALID_ENTITY(20),
+ SEARCHING_POSITION(5),
+ ENTITY_CREATION_ERROR(20),
+ TOO_MANY_ENTITIES(10);
+
+ private final int delayTicks;
+
+ DelayReason(int delayTicks) {
+ this.delayTicks = delayTicks;
+ }
+
+ public int getDelayTicks() {
+ return delayTicks;
+ }
+ }
+
+ private EntitySpawnResult spawnEntity() {
+ if (this.level == null) return new EntitySpawnResult.DelayResult(DelayReason.UNKNOWN);
+ var level = (ServerLevel) this.level;
+
+ var optEntityType = getEntityTypeFromStoredEntityData();
+ if (optEntityType.isEmpty()) return new EntitySpawnResult.DelayResult(DelayReason.INVALID_ENTITY);
+
+ var entityType = optEntityType.get();
+ var blockPos = getBlockPos();
+
+ var entity = entityType.create(level);
+ if (entity == null) return new EntitySpawnResult.DelayResult(DelayReason.ENTITY_CREATION_ERROR);
+
+ var nearbyEntities = level.getEntitiesOfClass(entity.getClass(), (new AABB(blockPos.getX(), blockPos.getY(), blockPos.getZ(), blockPos.getX() + 1, blockPos.getY() + 1, blockPos.getZ() + 1)).inflate(SPAWN_RANGE)).size();
+ if (nearbyEntities >= MAX_NEARBY_ENTITIES)
+ return new EntitySpawnResult.DelayResult(DelayReason.TOO_MANY_ENTITIES);
+
+ var random = level.getRandom();
+
+ var x = (double) blockPos.getX() + (random.nextDouble() - random.nextDouble()) * (double) SPAWN_RANGE + (double) 0.5F;
+ var y = blockPos.getY() + random.nextInt(3) - 1;
+ var z = (double) blockPos.getZ() + (random.nextDouble() - random.nextDouble()) * (double) SPAWN_RANGE + (double) 0.5F;
+
+ if (!level.noCollision(entityType.getAABB(x, y, z)))
+ return new EntitySpawnResult.DelayResult(DelayReason.SEARCHING_POSITION);
+
+ var yaw = Mth.wrapDegrees(random.nextFloat() * 360.0f);
+ entity.moveTo(x, y, z, yaw, 0);
+ if (entity instanceof Mob mob) {
+ if (!mob.checkSpawnObstruction(level))
+ return new EntitySpawnResult.DelayResult(DelayReason.SEARCHING_POSITION);
+
+ //noinspection deprecation,OverrideOnly
+ mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), MobSpawnType.SPAWNER, null, null);
+ }
+
+ level.addFreshEntity(entity);
+
+ level.gameEvent(entity, GameEvent.ENTITY_PLACE, BlockPos.containing(x, y, z));
+
+ return new EntitySpawnResult.SuccessfulResult(entity);
+ }
+
+ private void trySpawnEntity() {
+ if (level == null) return;
+
+ var result = spawnEntity();
+ if (result instanceof EntitySpawnResult.SuccessfulResult successfulResult) {
+ useFluid();
+ var center = getBlockPos().getCenter();
+ PacketHandler.sendToNearbyPlayers(new ClientboundSpawnerEventPacket(getBlockPos(), successfulResult.getEntity().getId()), center, 16, level.dimension());
+ spawnProgress = 0;
+ } else if (result instanceof EntitySpawnResult.DelayResult delayResult) {
+ delay(delayResult.getReason().getDelayTicks(), delayResult.getReason());
+ }
+ }
+
+ private enum ReasonForNotProgressing {
+ NO_SOUL,
+ NO_REGENERATION_POTION_LIQUID,
+ NOT_ENOUGH_REGENERATION_POTION_LIQUID,
+ NO_ROTATIONAL_FORCE,
+ }
+
+ private Optional getReasonForNotProgressing() {
+ if (speed == 0) {
+ return Optional.of(ReasonForNotProgressing.NO_ROTATIONAL_FORCE);
+ }
+
+ var fluid = tank.getPrimaryHandler().getFluid();
+ if (fluid.getTag() == null) {
+ return Optional.of(ReasonForNotProgressing.NO_REGENERATION_POTION_LIQUID);
+ }
+ if (fluid.getAmount() < getNecessaryFluidAmountForSpawning()) {
+ if (fluid.getAmount() == 0) {
+ return Optional.of(ReasonForNotProgressing.NO_REGENERATION_POTION_LIQUID);
+ }
+ return Optional.of(ReasonForNotProgressing.NOT_ENOUGH_REGENERATION_POTION_LIQUID);
+ }
+
+ if (storedEntityData.isEmpty()) {
+ return Optional.of(ReasonForNotProgressing.NO_SOUL);
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional getReasonForNotProgressingTranslationKey() {
+ var reason = getReasonForNotProgressing();
+ return reason.map(reasonForNotProgressing -> reasonForNotProgressing.name().toLowerCase());
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+
+ if (level == null) return;
+
+ if (level.isClientSide && fluidLevel != null) {
+ fluidLevel.tickChaser();
+ }
+
+ var unableToProgress = getReasonForNotProgressing().isPresent();
+ if (unableToProgress) return;
+
+ if (level.isClientSide) {
+ if (level.random.nextInt(6) == 0) {
+ ParticleUtils.drawParticles(ParticleTypes.ENTITY_EFFECT, (ClientLevel) level, getBlockPos().getCenter(), 3, 0.2, 0.2, 0.2, new Vec3(205 / 255.0, 92 / 255.0, 171 / 255.0));
+ }
+ return;
+ }
+
+ if (delayTicks != -1) {
+ delayTicks--;
+ if (delayTicks == 0) {
+ delayTicks = -1;
+ trySpawnEntity();
+ }
+ } else {
+ addProgressForTick();
+ if (spawnProgress >= 1.f) {
+ trySpawnEntity();
+ }
+ }
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/registry/ModBlockEntities.java b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/registry/ModBlockEntities.java
new file mode 100644
index 0000000..6134b9f
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/registry/ModBlockEntities.java
@@ -0,0 +1,20 @@
+package dev.kvnmtz.createmobspawners.blocks.entity.registry;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.blocks.entity.MechanicalSpawnerBlockEntity;
+import dev.kvnmtz.createmobspawners.blocks.registry.ModBlocks;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraftforge.eventbus.api.IEventBus;
+import net.minecraftforge.registries.DeferredRegister;
+import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraftforge.registries.RegistryObject;
+
+public class ModBlockEntities {
+ public static final DeferredRegister> BLOCK_ENTITIES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, CreateMobSpawners.MOD_ID);
+
+ public static final RegistryObject> SPAWNER_BE = BLOCK_ENTITIES.register("spawner_be", () -> BlockEntityType.Builder.of(MechanicalSpawnerBlockEntity::new, ModBlocks.SPAWNER.get()).build(null));
+
+ public static void register(IEventBus eventBus) {
+ BLOCK_ENTITIES.register(eventBus);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/renderer/MechanicalSpawnerBlockEntityRenderer.java b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/renderer/MechanicalSpawnerBlockEntityRenderer.java
new file mode 100644
index 0000000..8e9c2f2
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/blocks/entity/renderer/MechanicalSpawnerBlockEntityRenderer.java
@@ -0,0 +1,81 @@
+package dev.kvnmtz.createmobspawners.blocks.entity.renderer;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
+import com.simibubi.create.content.kinetics.base.KineticBlockEntityRenderer;
+import com.simibubi.create.foundation.fluid.FluidRenderer;
+import dev.kvnmtz.createmobspawners.blocks.entity.MechanicalSpawnerBlockEntity;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.core.Direction;
+import net.minecraft.util.Mth;
+import net.minecraft.world.item.ItemDisplayContext;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+
+@OnlyIn(Dist.CLIENT)
+public class MechanicalSpawnerBlockEntityRenderer extends KineticBlockEntityRenderer {
+ public MechanicalSpawnerBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
+ super(context);
+ }
+
+ @Override
+ protected BlockState getRenderedBlockState(MechanicalSpawnerBlockEntity be) {
+ return KineticBlockEntityRenderer.shaft(KineticBlockEntityRenderer.getRotationAxisOf(be));
+ }
+
+ @Override
+ protected void renderSafe(MechanicalSpawnerBlockEntity be, float partialTicks, PoseStack ms, MultiBufferSource buffer, int light, int overlay) {
+ if (be.getLevel() == null) return;
+
+ var vb = buffer.getBuffer(RenderType.solid());
+ KineticBlockEntityRenderer.renderRotatingKineticBlock(be, getRenderedBlockState(be), ms, vb, light);
+
+ if (be.hasStoredEntity()) {
+ var itemRenderer = Minecraft.getInstance().getItemRenderer();
+ var itemStack = ModItems.SOUL_CATCHER.get().getDefaultInstance();
+ ms.pushPose();
+ ms.translate(0.5f, 0.5f, 0.5f);
+ ms.scale(0.8f, 0.8f, 0.8f);
+ ms.mulPose(Axis.YP.rotationDegrees(getAngleForTe(be, be.getBlockPos(), Direction.Axis.Y) * 180 / (float) Math.PI));
+ itemRenderer.renderStatic(itemStack, ItemDisplayContext.FIXED, light, OverlayTexture.NO_OVERLAY, ms, buffer, be.getLevel(), 1);
+ ms.popPose();
+ }
+
+ var fluidLevel = be.getFluidLevel();
+ if (fluidLevel != null) {
+ var capHeight = 1.f/16.f + 1.f/128.f;
+ var tankHullWidth = 1.75f/16.f;
+ var minPuddleHeight = 1.f/16.f;
+ var height = 1.f;
+ var width = 1.f;
+ var totalHeight = height - 2 * capHeight - minPuddleHeight;
+ var level = fluidLevel.getValue(partialTicks);
+ if (!(level < 1.0F / (512 * totalHeight))) {
+ var clampedLevel = Mth.clamp(level * totalHeight, 0.0F, totalHeight);
+ var fluidStack = be.getFluidStack();
+ if (!fluidStack.isEmpty()) {
+ var top = fluidStack.getFluid().getFluidType().isLighterThanAir();
+ var xMax = tankHullWidth + width - 2 * tankHullWidth;
+ var yMin = totalHeight + capHeight + minPuddleHeight - clampedLevel;
+ var yMax = yMin + clampedLevel;
+ if (top) {
+ yMin += totalHeight - clampedLevel;
+ yMax += totalHeight - clampedLevel;
+ }
+
+ var zMax = tankHullWidth + width - 2 * tankHullWidth;
+ ms.pushPose();
+ ms.translate(0.0F, clampedLevel - totalHeight, 0.0F);
+ FluidRenderer.renderFluidBox(fluidStack, tankHullWidth, yMin, tankHullWidth, xMax, yMax, zMax, buffer, ms, light, false);
+ ms.popPose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/blocks/registry/ModBlocks.java b/src/main/java/dev/kvnmtz/createmobspawners/blocks/registry/ModBlocks.java
new file mode 100644
index 0000000..0a05423
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/blocks/registry/ModBlocks.java
@@ -0,0 +1,41 @@
+package dev.kvnmtz.createmobspawners.blocks.registry;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.blocks.MechanicalSpawnerBlock;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import net.minecraft.world.item.BlockItem;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.Rarity;
+import net.minecraft.world.level.block.Block;
+import net.minecraftforge.eventbus.api.IEventBus;
+import net.minecraftforge.registries.DeferredRegister;
+import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraftforge.registries.RegistryObject;
+
+import java.util.function.Supplier;
+
+public class ModBlocks {
+ public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, CreateMobSpawners.MOD_ID);
+
+ public static final RegistryObject SPAWNER = registerBlock("mechanical_spawner", MechanicalSpawnerBlock::new, new Item.Properties().rarity(Rarity.UNCOMMON));
+
+ private static RegistryObject registerBlock(String name, Supplier block) {
+ var toReturn = BLOCKS.register(name, block);
+ registerBlockItem(name, toReturn, new Item.Properties());
+ return toReturn;
+ }
+
+ private static RegistryObject registerBlock(String name, Supplier block, Item.Properties itemProperties) {
+ var toReturn = BLOCKS.register(name, block);
+ registerBlockItem(name, toReturn, itemProperties);
+ return toReturn;
+ }
+
+ private static RegistryObject
- registerBlockItem(String name, RegistryObject block, Item.Properties itemProperties) {
+ return ModItems.ITEMS.register(name, () -> new BlockItem(block.get(), itemProperties));
+ }
+
+ public static void register(IEventBus eventBus) {
+ BLOCKS.register(eventBus);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStack.java b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStack.java
new file mode 100644
index 0000000..29be3e8
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStack.java
@@ -0,0 +1,32 @@
+package dev.kvnmtz.createmobspawners.capabilities.entitystorage;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.item.BlockItem;
+import net.minecraft.world.item.ItemStack;
+
+public class EntityStorageItemStack implements IEntityStorage {
+ private final ItemStack stack;
+
+ public EntityStorageItemStack(ItemStack stack) {
+ this.stack = stack;
+ }
+
+ @Override
+ public StoredEntityData getStoredEntityData() {
+ var tag = stack.getOrCreateTag();
+ var entity = StoredEntityData.empty();
+ if (tag.contains(BlockItem.BLOCK_ENTITY_TAG)) {
+ var entityTag = tag.getCompound(BlockItem.BLOCK_ENTITY_TAG).getCompound("EntityStorage");
+ entity.deserializeNBT(entityTag);
+ }
+ return entity;
+ }
+
+ @Override
+ public void setStoredEntityData(StoredEntityData entity) {
+ var tag = stack.getOrCreateTag();
+ var entityTag = new CompoundTag();
+ entityTag.put("EntityStorage", entity.serializeNBT());
+ tag.put(BlockItem.BLOCK_ENTITY_TAG, entityTag);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStackCapabilityProvider.java b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStackCapabilityProvider.java
new file mode 100644
index 0000000..cb1266f
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/EntityStorageItemStackCapabilityProvider.java
@@ -0,0 +1,25 @@
+package dev.kvnmtz.createmobspawners.capabilities.entitystorage;
+
+import dev.kvnmtz.createmobspawners.capabilities.registry.ModCapabilities;
+import net.minecraft.core.Direction;
+import net.minecraft.world.item.ItemStack;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.ICapabilityProvider;
+import net.minecraftforge.common.util.LazyOptional;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class EntityStorageItemStackCapabilityProvider implements ICapabilityProvider {
+ private final EntityStorageItemStack backend;
+ private final LazyOptional optionalData;
+
+ public EntityStorageItemStackCapabilityProvider(ItemStack stack) {
+ this.backend = new EntityStorageItemStack(stack);
+ this.optionalData = LazyOptional.of(() -> backend);
+ }
+
+ @Override
+ public @NotNull LazyOptional getCapability(@NotNull Capability capability, @Nullable Direction direction) {
+ return ModCapabilities.ENTITY_STORAGE.orEmpty(capability, optionalData.cast());
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/IEntityStorage.java b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/IEntityStorage.java
new file mode 100644
index 0000000..bf885c6
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/IEntityStorage.java
@@ -0,0 +1,11 @@
+package dev.kvnmtz.createmobspawners.capabilities.entitystorage;
+
+public interface IEntityStorage {
+ default boolean hasStoredEntity() {
+ return getStoredEntityData().getEntityType().isPresent();
+ }
+
+ StoredEntityData getStoredEntityData();
+
+ void setStoredEntityData(StoredEntityData entity);
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/StoredEntityData.java b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/StoredEntityData.java
new file mode 100644
index 0000000..2e90f6e
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/entitystorage/StoredEntityData.java
@@ -0,0 +1,97 @@
+package dev.kvnmtz.createmobspawners.capabilities.entitystorage;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraftforge.common.extensions.IForgeEntity;
+import net.minecraftforge.common.util.INBTSerializable;
+
+import java.util.Optional;
+
+/**
+ * Credit goes to EnderIO for entity data serialization and basic soul vial functionality
+ */
+public class StoredEntityData implements INBTSerializable {
+ private CompoundTag entityTag = new CompoundTag();
+ private float maxHealth = 0.0f;
+
+ /**
+ * Should match key from {@link IForgeEntity#serializeNBT()}.
+ */
+ public static final String KEY_ID = "id";
+
+ /**
+ * Should match key from {@link Entity#saveWithoutId(CompoundTag)}
+ */
+ public static final String KEY_ENTITY = "Entity";
+ private static final String KEY_MAX_HEALTH = "MaxHealth";
+
+ public StoredEntityData() {
+ }
+
+ public static StoredEntityData of(LivingEntity entity) {
+ var data = new StoredEntityData();
+ data.entityTag = entity.serializeNBT();
+ data.maxHealth = entity.getMaxHealth();
+ return data;
+ }
+
+ public static StoredEntityData empty() {
+ var data = new StoredEntityData();
+ data.maxHealth = 0.0f;
+ return data;
+ }
+
+ public boolean isEmpty() {
+ return maxHealth == 0.0f;
+ }
+
+ public Optional getEntityType() {
+ var tag = entityTag;
+ if (tag.contains(KEY_ID)) {
+ return Optional.of(new ResourceLocation(tag.getString(KEY_ID)));
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional getEntityDisplayName() {
+ var optEntityTypeResourceLocation = getEntityType();
+ if (optEntityTypeResourceLocation.isPresent()) {
+ var optEntityType = EntityType.byString(optEntityTypeResourceLocation.get().toString());
+ if (optEntityType.isPresent()) {
+ var entityType = optEntityType.get();
+ return Optional.of(entityType.getDescription().getString());
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ public CompoundTag getEntityTag() {
+ return entityTag;
+ }
+
+ @Override
+ public Tag serializeNBT() {
+ var compound = new CompoundTag();
+ compound.put(KEY_ENTITY, entityTag);
+ if (maxHealth > 0.0f) {
+ compound.putFloat(KEY_MAX_HEALTH, maxHealth);
+ }
+ return compound;
+ }
+
+ @Override
+ public void deserializeNBT(Tag tag) {
+ if (tag instanceof CompoundTag compoundTag) {
+ entityTag = compoundTag.getCompound(KEY_ENTITY);
+ if (compoundTag.contains(KEY_MAX_HEALTH)) {
+ maxHealth = compoundTag.getFloat(KEY_MAX_HEALTH);
+ }
+ }
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/capabilities/registry/ModCapabilities.java b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/registry/ModCapabilities.java
new file mode 100644
index 0000000..03e1775
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/capabilities/registry/ModCapabilities.java
@@ -0,0 +1,20 @@
+package dev.kvnmtz.createmobspawners.capabilities.registry;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.IEntityStorage;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.CapabilityManager;
+import net.minecraftforge.common.capabilities.CapabilityToken;
+import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+
+@Mod.EventBusSubscriber(modid = CreateMobSpawners.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
+public class ModCapabilities {
+ public static final Capability ENTITY_STORAGE = CapabilityManager.get(new CapabilityToken<>() {});
+
+ @SubscribeEvent
+ public static void register(RegisterCapabilitiesEvent event) {
+ event.register(IEntityStorage.class);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/items/ModCreativeModeTabs.java b/src/main/java/dev/kvnmtz/createmobspawners/items/ModCreativeModeTabs.java
new file mode 100644
index 0000000..8ea4c97
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/items/ModCreativeModeTabs.java
@@ -0,0 +1,29 @@
+package dev.kvnmtz.createmobspawners.items;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.blocks.registry.ModBlocks;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.item.CreativeModeTab;
+import net.minecraftforge.eventbus.api.IEventBus;
+import net.minecraftforge.registries.DeferredRegister;
+import net.minecraftforge.registries.RegistryObject;
+
+public class ModCreativeModeTabs {
+ public static final DeferredRegister CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, CreateMobSpawners.MOD_ID);
+
+ public static final RegistryObject CREATE_MOB_SPAWNERS_TAB =
+ CREATIVE_MODE_TABS.register("create_mob_spawners_tab", () -> CreativeModeTab.builder()
+ .icon(() -> ModItems.EMPTY_SOUL_CATCHER.get().getDefaultInstance())
+ .title(Component.translatable("creativetab.create_mob_spawners_tab"))
+ .displayItems((itemDisplayParameters, output) -> {
+ output.accept(ModItems.EMPTY_SOUL_CATCHER.get());
+ output.accept(ModBlocks.SPAWNER.get());
+ }).build()
+ );
+
+ public static void register(IEventBus eventBus) {
+ CREATIVE_MODE_TABS.register(eventBus);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/items/SoulCatcherItem.java b/src/main/java/dev/kvnmtz/createmobspawners/items/SoulCatcherItem.java
new file mode 100644
index 0000000..c9a3e0e
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/items/SoulCatcherItem.java
@@ -0,0 +1,416 @@
+package dev.kvnmtz.createmobspawners.items;
+
+import com.simibubi.create.foundation.item.render.SimpleCustomRenderer;
+import dev.kvnmtz.createmobspawners.Config;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.EntityStorageItemStackCapabilityProvider;
+import dev.kvnmtz.createmobspawners.capabilities.registry.ModCapabilities;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.IEntityStorage;
+import dev.kvnmtz.createmobspawners.capabilities.entitystorage.StoredEntityData;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import dev.kvnmtz.createmobspawners.items.renderer.SoulCatcherRenderer;
+import dev.kvnmtz.createmobspawners.network.ClientboundEntityReleasePacket;
+import dev.kvnmtz.createmobspawners.network.ClientboundEntityCatchPacket;
+import dev.kvnmtz.createmobspawners.network.PacketHandler;
+import dev.kvnmtz.createmobspawners.utils.BoundingBoxUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.effect.MobEffects;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.context.UseOnContext;
+import net.minecraft.world.level.Level;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import net.minecraftforge.client.event.RenderLivingEvent;
+import net.minecraftforge.client.extensions.common.IClientItemExtensions;
+import net.minecraftforge.common.Tags;
+import net.minecraftforge.common.capabilities.ICapabilityProvider;
+import net.minecraftforge.common.extensions.IForgeItem;
+import net.minecraftforge.event.TickEvent;
+import net.minecraftforge.event.entity.player.ItemTooltipEvent;
+import net.minecraftforge.event.entity.player.PlayerEvent;
+import net.minecraftforge.event.entity.player.PlayerInteractEvent;
+import net.minecraftforge.event.server.ServerStoppingEvent;
+import net.minecraftforge.eventbus.api.EventPriority;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+public class SoulCatcherItem extends Item implements IForgeItem {
+ public SoulCatcherItem() {
+ super(new Item.Properties().stacksTo(1));
+ }
+
+ private static class ShrinkingEntityData {
+ private final Player player;
+ private final ItemStack itemStack;
+ private final long startTime;
+ private float nextLineAfterElapsedSeconds;
+ private final boolean hadAi;
+
+ public ShrinkingEntityData(Player player, ItemStack itemStack, boolean hadAi) {
+ this.player = player;
+ this.itemStack = itemStack;
+ this.startTime = System.currentTimeMillis();
+ this.nextLineAfterElapsedSeconds = CATCHING_LINE_DRAW_DELAY;
+ this.hadAi = hadAi;
+ }
+ }
+
+ private static final HashMap shrinkingEntities = new HashMap<>();
+
+ private static final HashMap shrinkingEntitiesToStartTimeMap = new HashMap<>();
+
+ public static void addShrinkingEntity(Entity entity) {
+ shrinkingEntitiesToStartTimeMap.put(entity, System.currentTimeMillis());
+ }
+
+ public static void removeShrinkingEntity(Entity entity) {
+ shrinkingEntitiesToStartTimeMap.remove(entity);
+ }
+
+ private static final float CATCHING_LINE_DRAW_DELAY = 0.2f;
+ private static final float MAX_DISTANCE = 10.f;
+
+ @OnlyIn(Dist.CLIENT)
+ @SubscribeEvent
+ protected static void onRenderEntity(RenderLivingEvent.Pre, ?> event) {
+ var entity = event.getEntity();
+ if (!shrinkingEntitiesToStartTimeMap.containsKey(entity)) return;
+
+ var startTime = shrinkingEntitiesToStartTimeMap.get(entity);
+
+ var currentTime = System.currentTimeMillis();
+ var elapsedTime = (currentTime - startTime) / 1000.0f;
+
+ var scaleFactor = 1.f - (elapsedTime / getCatchingDuration(entity));
+ scaleFactor = Math.max(scaleFactor, 0);
+
+ var translationHeight = entity.getBbHeight() / 2;
+ event.getPoseStack().translate(0, translationHeight - scaleFactor * translationHeight, 0);
+ event.getPoseStack().scale(scaleFactor, scaleFactor, scaleFactor);
+ }
+
+ @SubscribeEvent
+ protected static void onServerTick(TickEvent.ServerTickEvent event) {
+ for (var shrinkingEntity : shrinkingEntities.keySet()) {
+ var data = shrinkingEntities.get(shrinkingEntity);
+ var player = data.player;
+ var itemStack = data.itemStack;
+
+ var currentItem = player.getMainHandItem();
+
+ if (!currentItem.equals(itemStack, true) || !isItemAbleToCatch(currentItem)) {
+ cancelCatch(shrinkingEntity);
+ continue;
+ }
+ if (!isEntityCatchable(player, shrinkingEntity, component -> player.displayClientMessage(component, true))) {
+ cancelCatch(shrinkingEntity);
+ continue;
+ }
+
+ var currentTime = System.currentTimeMillis();
+ var elapsedTime = (currentTime - data.startTime) / 1000.0f;
+
+ if (elapsedTime >= data.nextLineAfterElapsedSeconds) {
+ PacketHandler.sendToNearbyPlayers(new ClientboundEntityCatchPacket(shrinkingEntity.getId(), player.getId(), ClientboundEntityCatchPacket.EntityCatchState.IN_PROGRESS), player.getEyePosition(), 16, player.level().dimension());
+ data.nextLineAfterElapsedSeconds += CATCHING_LINE_DRAW_DELAY;
+ }
+
+ if (elapsedTime >= getCatchingDuration(shrinkingEntity)) {
+ onShrinkComplete(shrinkingEntity);
+ }
+ }
+ }
+
+ @SubscribeEvent(priority = EventPriority.LOWEST)
+ protected static void addMobTooltip(ItemTooltipEvent event) {
+ var itemStack = event.getItemStack();
+ if (itemStack.getItem() != ModItems.SOUL_CATCHER.get()) return;
+
+ var hasEntityData = getEntityData(itemStack).isPresent();
+ if (!hasEntityData) return;
+
+ var displayName = getEntityData(itemStack).get().getEntityDisplayName();
+ if (displayName.isEmpty()) return;
+
+ event.getToolTip().add(1, Component.literal(displayName.get()).withStyle(ChatFormatting.GRAY));
+ }
+
+ @SubscribeEvent
+ protected static void onServerClose(ServerStoppingEvent event) {
+ for (var shrinkingEntity : shrinkingEntities.keySet()) {
+ cancelCatch(shrinkingEntity);
+ }
+ }
+
+ @SubscribeEvent
+ protected static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event) {
+ var player = event.getEntity();
+ for (var entry : shrinkingEntities.entrySet()) {
+ if (player != entry.getValue().player) continue;
+ cancelCatch(entry.getKey());
+ }
+ }
+
+ @SubscribeEvent
+ protected static void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
+ var player = event.getEntity();
+ for (var entry : shrinkingEntities.entrySet()) {
+ if (player != entry.getValue().player) continue;
+ cancelCatch(entry.getKey());
+ }
+ }
+
+ // instead of overriding interactLivingEntity, this event is necessary to block interactions with entities like villagers, wolves, donkeys...
+ @SubscribeEvent(priority = EventPriority.LOWEST)
+ protected static void onRightClickEntity(PlayerInteractEvent.EntityInteract event) {
+ if (!(event.getLevel() instanceof ServerLevel)) return;
+
+ var itemStack = event.getItemStack();
+ if (itemStack.getItem() != ModItems.EMPTY_SOUL_CATCHER.get()) return;
+
+ event.setCancellationResult(InteractionResult.PASS);
+ event.setCanceled(true);
+
+ var player = event.getEntity();
+ var targetEntity = event.getTarget();
+
+ if (!(targetEntity instanceof LivingEntity target)) return;
+
+ if (!isItemAbleToCatch(itemStack)) return;
+ if (!isEntityCatchable(player, target, component -> player.displayClientMessage(component, true)))
+ return;
+
+ if (shrinkingEntities.containsKey(target)) return;
+ for (var shrinkingEntityData : shrinkingEntities.values()) {
+ if (shrinkingEntityData.player.equals(player)) {
+ return;
+ }
+ }
+
+ startCatchingEntity(target, player, itemStack);
+ }
+
+ private static void cancelCatch(LivingEntity entity) {
+ var data = shrinkingEntities.get(entity);
+ if (data.hadAi && entity instanceof Mob mob) {
+ mob.setNoAi(false);
+ }
+
+ PacketHandler.sendToAllPlayers(new ClientboundEntityCatchPacket(entity.getId(), data.player.getId(), ClientboundEntityCatchPacket.EntityCatchState.CANCELED));
+ shrinkingEntities.remove(entity);
+ }
+
+ private static float getCatchingDuration(Entity entity) {
+ var boundingBox = entity.getBoundingBox();
+ var volume = BoundingBoxUtils.getBoundingBoxVolume(boundingBox);
+ return (float) (1.3811 * Math.pow(volume, 0.5026));
+ }
+
+ private static void onShrinkComplete(LivingEntity entity) {
+ var data = shrinkingEntities.get(entity);
+ if (data == null) return;
+
+ PacketHandler.sendToAllPlayers(new ClientboundEntityCatchPacket(entity.getId(), data.player.getId(), ClientboundEntityCatchPacket.EntityCatchState.FINISHED));
+
+ var newItemStack = catchEntity(data.itemStack, entity, data.hadAi);
+
+ var player = data.player;
+ player.setItemInHand(InteractionHand.MAIN_HAND, newItemStack);
+
+ shrinkingEntities.remove(entity);
+ }
+
+ private static void startCatchingEntity(LivingEntity entity, Player player, ItemStack itemStack) {
+ var hadAi = false;
+ if (entity instanceof Mob mob) {
+ hadAi = !mob.isNoAi();
+ }
+ shrinkingEntities.put(entity, new ShrinkingEntityData(player, itemStack, hadAi));
+
+ if (entity instanceof Mob mob) {
+ mob.setNoAi(true);
+ }
+
+ PacketHandler.sendToAllPlayers(new ClientboundEntityCatchPacket(entity.getId(), player.getId(), ClientboundEntityCatchPacket.EntityCatchState.STARTED));
+ }
+
+ @Override
+ public @NotNull InteractionResult useOn(UseOnContext pContext) {
+ if (pContext.getLevel().isClientSide) {
+ return InteractionResult.FAIL;
+ }
+
+ var player = pContext.getPlayer();
+ if (player == null) {
+ return InteractionResult.FAIL;
+ }
+
+ var optEntity = releaseEntity(pContext.getLevel(), pContext.getItemInHand(), pContext.getClickedFace(), pContext.getClickedPos(), emptyCatcher -> player.setItemInHand(pContext.getHand(), emptyCatcher));
+ optEntity.ifPresent(entity -> PacketHandler.sendToNearbyPlayers(new ClientboundEntityReleasePacket(entity.getId(), player.getId()), entity.position(), 16, entity.level().dimension()));
+
+ return InteractionResult.SUCCESS;
+ }
+
+ public enum CapturableStatus {
+ CAPTURABLE(Component.empty()),
+ BOSS(Component.translatable("item.create_mob_spawners.empty_soul_catcher.capturable_status.boss").withStyle(ChatFormatting.RED)),
+ BLACKLISTED(Component.translatable("item.create_mob_spawners.empty_soul_catcher.capturable_status.blacklisted").withStyle(ChatFormatting.RED)),
+ INCOMPATIBLE(Component.translatable("item.create_mob_spawners.empty_soul_catcher.capturable_status.incompatible").withStyle(ChatFormatting.RED));
+
+ CapturableStatus(Component errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ private final Component errorMessage;
+
+ public Component errorMessage() {
+ return errorMessage;
+ }
+ }
+
+ private static CapturableStatus getCapturableStatus(EntityType extends LivingEntity> type, @Nullable Entity entity) {
+ if (entity != null && isBlacklistedBoss(entity)) {
+ return CapturableStatus.BOSS;
+ }
+
+ if (!type.canSerialize()) {
+ return CapturableStatus.INCOMPATIBLE;
+ }
+
+ if (Config.soulCatcherEntityBlacklist.contains(type)) {
+ return CapturableStatus.BLACKLISTED;
+ }
+
+ return CapturableStatus.CAPTURABLE;
+ }
+
+ public static boolean isBlacklistedBoss(Entity entity) {
+ return entity.getType().is(Tags.EntityTypes.BOSSES);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private static boolean isItemAbleToCatch(ItemStack soulCatcher) {
+ var entityData = getEntityData(soulCatcher);
+ return entityData.isEmpty() || entityData.get().getEntityType().isEmpty();
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private static boolean isEntityCatchable(Player player, LivingEntity entity, Consumer displayCallback) {
+ final var capturableStatusKeyPrefix = "item.create_mob_spawners.empty_soul_catcher.capturable_status.";
+
+ if (entity instanceof Player) {
+ displayCallback.accept(Component.translatable(capturableStatusKeyPrefix + "player").withStyle(ChatFormatting.RED));
+ return false;
+ }
+
+ //noinspection unchecked
+ var status = getCapturableStatus((EntityType extends LivingEntity>) entity.getType(), entity);
+ if (status != CapturableStatus.CAPTURABLE) {
+ displayCallback.accept(status.errorMessage());
+ return false;
+ }
+
+ if (!entity.hasEffect(MobEffects.WEAKNESS)) {
+ displayCallback.accept(Component.translatable(capturableStatusKeyPrefix + "no_weakness").withStyle(ChatFormatting.RED));
+ return false;
+ }
+
+ if (!entity.isAlive()) {
+ displayCallback.accept(Component.translatable(capturableStatusKeyPrefix + "dead").withStyle(ChatFormatting.RED));
+ return false;
+ }
+
+ if (entity.distanceTo(player) > MAX_DISTANCE) {
+ displayCallback.accept(Component.translatable(capturableStatusKeyPrefix + "too_far").withStyle(ChatFormatting.RED));
+ return false;
+ }
+
+ return true;
+ }
+
+ private static ItemStack catchEntity(ItemStack itemStack, LivingEntity entity, boolean hadAi) {
+ if (entity instanceof Mob mob) {
+ if (mob.getLeashHolder() != null) {
+ mob.dropLeash(true, true);
+ }
+ if (hadAi) {
+ mob.setNoAi(false);
+ }
+ }
+
+ itemStack.shrink(1);
+ var catcher = ModItems.SOUL_CATCHER.get().getDefaultInstance();
+ setEntityData(catcher, entity);
+
+ entity.discard();
+
+ return catcher;
+ }
+
+ private static Optional releaseEntity(Level level, ItemStack catcher, Direction face, BlockPos pos, Consumer emptyCatcherSetter) {
+ var spawnedEntity = new AtomicReference>(Optional.empty());
+
+ catcher.getCapability(ModCapabilities.ENTITY_STORAGE).ifPresent(entityStorage -> {
+ if (entityStorage.hasStoredEntity()) {
+ var entityData = entityStorage.getStoredEntityData();
+
+ var spawnX = pos.getX() + face.getStepX() + 0.5;
+ var spawnY = pos.getY() + face.getStepY();
+ var spawnZ = pos.getZ() + face.getStepZ() + 0.5;
+
+ var rotation = Mth.wrapDegrees(level.getRandom().nextFloat() * 360.0f);
+
+ var optEntity = EntityType.create(entityData.getEntityTag(), level);
+
+ optEntity.ifPresent(ent -> {
+ ent.setPos(spawnX, spawnY, spawnZ);
+ ent.setYRot(rotation);
+ level.addFreshEntity(ent);
+ });
+ emptyCatcherSetter.accept(ModItems.EMPTY_SOUL_CATCHER.get().getDefaultInstance());
+
+ spawnedEntity.set(optEntity);
+ }
+ });
+
+ return spawnedEntity.get();
+ }
+
+ private static void setEntityData(ItemStack stack, LivingEntity entity) {
+ stack.getCapability(ModCapabilities.ENTITY_STORAGE).ifPresent(storage -> storage.setStoredEntityData(StoredEntityData.of(entity)));
+ }
+
+ public static Optional getEntityData(ItemStack stack) {
+ return stack.getCapability(ModCapabilities.ENTITY_STORAGE).map(IEntityStorage::getStoredEntityData);
+ }
+
+ @Override
+ public @Nullable ICapabilityProvider initCapabilities(ItemStack stack, @Nullable CompoundTag nbt) {
+ return new EntityStorageItemStackCapabilityProvider(stack);
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ @Override
+ public void initializeClient(Consumer consumer) {
+ consumer.accept(SimpleCustomRenderer.create(this, new SoulCatcherRenderer()));
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/items/registry/ModItems.java b/src/main/java/dev/kvnmtz/createmobspawners/items/registry/ModItems.java
new file mode 100644
index 0000000..fa39c3a
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/items/registry/ModItems.java
@@ -0,0 +1,20 @@
+package dev.kvnmtz.createmobspawners.items.registry;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.items.SoulCatcherItem;
+import net.minecraft.world.item.Item;
+import net.minecraftforge.eventbus.api.IEventBus;
+import net.minecraftforge.registries.DeferredRegister;
+import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraftforge.registries.RegistryObject;
+
+public class ModItems {
+ public static final DeferredRegister
- ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, CreateMobSpawners.MOD_ID);
+
+ public static final RegistryObject
- EMPTY_SOUL_CATCHER = ITEMS.register("empty_soul_catcher", SoulCatcherItem::new);
+ public static final RegistryObject
- SOUL_CATCHER = ITEMS.register("soul_catcher", SoulCatcherItem::new);
+
+ public static void register(IEventBus eventBus) {
+ ITEMS.register(eventBus);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/items/renderer/SoulCatcherRenderer.java b/src/main/java/dev/kvnmtz/createmobspawners/items/renderer/SoulCatcherRenderer.java
new file mode 100644
index 0000000..80e006f
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/items/renderer/SoulCatcherRenderer.java
@@ -0,0 +1,58 @@
+package dev.kvnmtz.createmobspawners.items.renderer;
+
+import com.jozufozu.flywheel.core.PartialModel;
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
+import com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueHandler;
+import com.simibubi.create.foundation.item.render.CustomRenderedItemModel;
+import com.simibubi.create.foundation.item.render.CustomRenderedItemModelRenderer;
+import com.simibubi.create.foundation.item.render.PartialItemModelRenderer;
+import com.simibubi.create.foundation.utility.AnimationTickHolder;
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.items.registry.ModItems;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.world.item.ItemDisplayContext;
+import net.minecraft.world.item.ItemStack;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+
+@OnlyIn(Dist.CLIENT)
+public class SoulCatcherRenderer extends CustomRenderedItemModelRenderer {
+ protected static final PartialModel GEAR = new PartialModel(CreateMobSpawners.asResource("item/soul_catcher/gear"));
+ protected static final PartialModel GEAR_EMPTY = new PartialModel(CreateMobSpawners.asResource("item/soul_catcher/gear_empty"));
+
+ @Override
+ protected void render(ItemStack itemStack, CustomRenderedItemModel model, PartialItemModelRenderer renderer, ItemDisplayContext itemDisplayContext, PoseStack ms, MultiBufferSource multiBufferSource, int light, int overlay) {
+ var isEmpty = itemStack.getItem() == ModItems.EMPTY_SOUL_CATCHER.get();
+
+ renderer.render(model.getOriginalModel(), light);
+
+ if (isEmpty) {
+ // X pivot is already 8
+ var yOffset = -2.6515/16f;
+ var zOffset = 1.6515/16f;
+
+ // Y & Z pivots need to be centered (8) in order to rotate around X
+ ms.translate(0, -yOffset, -zOffset);
+ ms.mulPose(Axis.XP.rotationDegrees(45));
+ ms.translate(0, yOffset, zOffset);
+
+ // X & Y pivots need to be centered (8) in order to rotate around Z
+ ms.translate(0, -yOffset, 0);
+ ms.mulPose(Axis.ZP.rotationDegrees(ScrollValueHandler.getScroll(AnimationTickHolder.getPartialTicks()) * 15));
+ ms.translate(0, yOffset, 0);
+
+ renderer.render(GEAR_EMPTY.get(), light);
+ } else {
+ // X pivot is already 8
+ var yOffset = 3.5/16f;
+
+ // X & Y pivots need to be centered (8) in order to rotate around Z
+ ms.translate(0, -yOffset, 0);
+ ms.mulPose(Axis.ZP.rotationDegrees(ScrollValueHandler.getScroll(AnimationTickHolder.getPartialTicks()) * 15));
+ ms.translate(0, yOffset, 0);
+
+ renderer.render(GEAR.get(), light);
+ }
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityCatchPacket.java b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityCatchPacket.java
new file mode 100644
index 0000000..c743d03
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityCatchPacket.java
@@ -0,0 +1,82 @@
+package dev.kvnmtz.createmobspawners.network;
+
+import dev.kvnmtz.createmobspawners.items.SoulCatcherItem;
+import dev.kvnmtz.createmobspawners.utils.ParticleUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.fml.DistExecutor;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.function.Supplier;
+
+public class ClientboundEntityCatchPacket {
+ public enum EntityCatchState {
+ STARTED,
+ IN_PROGRESS,
+ FINISHED,
+ CANCELED,
+ }
+
+ private final int entityId;
+ private final int playerId;
+ private final EntityCatchState state;
+
+ public ClientboundEntityCatchPacket(int entityId, int playerId, EntityCatchState state) {
+ this.entityId = entityId;
+ this.playerId = playerId;
+ this.state = state;
+ }
+
+ public ClientboundEntityCatchPacket(FriendlyByteBuf buffer) {
+ this(buffer.readInt(), buffer.readInt(), EntityCatchState.values()[buffer.readByte()]);
+ }
+
+ public void encode(FriendlyByteBuf buffer) {
+ buffer.writeInt(entityId);
+ buffer.writeInt(playerId);
+ buffer.writeByte(state.ordinal());
+ }
+
+ public void handle(Supplier ctx) {
+ ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
+ var level = Minecraft.getInstance().level;
+ if (level == null) return;
+
+ var entity = Minecraft.getInstance().level.getEntity(entityId);
+ if (entity == null) return;
+
+ if (state == EntityCatchState.CANCELED) {
+ SoulCatcherItem.removeShrinkingEntity(entity);
+ return;
+ }
+
+ var player = Minecraft.getInstance().level.getEntity(playerId);
+ if (player == null) return;
+
+ var entityBoundingBox = entity.getBoundingBox();
+ var entityCenter = entityBoundingBox.getCenter();
+ var playerCenter = player.getBoundingBox().getCenter();
+ var direction = entityCenter.subtract(playerCenter).normalize();
+ var pointInFrontOfPlayer = playerCenter.add(direction.multiply(0.66f, 0.66f, 0.66f));
+
+ switch (state) {
+ case STARTED:
+ SoulCatcherItem.addShrinkingEntity(entity);
+ ParticleUtils.drawParticleLine(ParticleTypes.WITCH, level, entityBoundingBox.getCenter(), pointInFrontOfPlayer, 0.5, Vec3.ZERO);
+ ParticleUtils.drawParticles(ParticleTypes.WITCH, level, entityCenter, ParticleUtils.getParticleCountForEntity(entity), entityBoundingBox.getXsize() / 3, entityBoundingBox.getYsize() / 3, entityBoundingBox.getZsize() / 3, Vec3.ZERO);
+ break;
+ case IN_PROGRESS:
+ ParticleUtils.drawParticleLine(ParticleTypes.WITCH, level, entityBoundingBox.getCenter(), pointInFrontOfPlayer, 0.5, Vec3.ZERO);
+ break;
+ case FINISHED:
+ SoulCatcherItem.removeShrinkingEntity(entity);
+ ParticleUtils.drawParticles(ParticleTypes.REVERSE_PORTAL, level, entityCenter, ParticleUtils.getParticleCountForEntity(entity), entityBoundingBox.getXsize() / 3, entityBoundingBox.getYsize() / 3, entityBoundingBox.getZsize() / 3, Vec3.ZERO);
+ break;
+ }
+ }));
+ ctx.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityReleasePacket.java b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityReleasePacket.java
new file mode 100644
index 0000000..7ad6d0d
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundEntityReleasePacket.java
@@ -0,0 +1,54 @@
+package dev.kvnmtz.createmobspawners.network;
+
+import dev.kvnmtz.createmobspawners.utils.ParticleUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.fml.DistExecutor;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.function.Supplier;
+
+public class ClientboundEntityReleasePacket {
+ private final int entityId;
+ private final int playerId;
+
+ public ClientboundEntityReleasePacket(int entityId, int playerId) {
+ this.entityId = entityId;
+ this.playerId = playerId;
+ }
+
+ public ClientboundEntityReleasePacket(FriendlyByteBuf buffer) {
+ this(buffer.readInt(), buffer.readInt());
+ }
+
+ public void encode(FriendlyByteBuf buffer) {
+ buffer.writeInt(entityId);
+ buffer.writeInt(playerId);
+ }
+
+ public void handle(Supplier ctx) {
+ ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
+ var level = Minecraft.getInstance().level;
+ if (level == null) return;
+
+ var entity = Minecraft.getInstance().level.getEntity(entityId);
+ if (entity == null) return;
+
+ var player = Minecraft.getInstance().level.getEntity(playerId);
+ if (player == null) return;
+
+ var entityBoundingBox = entity.getBoundingBox();
+ var entityCenter = entityBoundingBox.getCenter();
+ var playerCenter = player.getBoundingBox().getCenter();
+ var direction = entityCenter.subtract(playerCenter).normalize();
+ var pointInFrontOfPlayer = playerCenter.add(direction.multiply(0.66f, 0.66f, 0.66f));
+
+ ParticleUtils.drawParticleLine(ParticleTypes.WITCH, level, entityCenter, pointInFrontOfPlayer, 0.5, Vec3.ZERO);
+ ParticleUtils.drawParticles(ParticleTypes.WITCH, level, entityCenter, ParticleUtils.getParticleCountForEntity(entity), entityBoundingBox.getXsize() / 3, entityBoundingBox.getYsize() / 3, entityBoundingBox.getZsize() / 3, Vec3.ZERO);
+ }));
+ ctx.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundSpawnerEventPacket.java b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundSpawnerEventPacket.java
new file mode 100644
index 0000000..1777828
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/network/ClientboundSpawnerEventPacket.java
@@ -0,0 +1,51 @@
+package dev.kvnmtz.createmobspawners.network;
+
+import dev.kvnmtz.createmobspawners.utils.ParticleUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.fml.DistExecutor;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.function.Supplier;
+
+public class ClientboundSpawnerEventPacket {
+ private final BlockPos spawnerPosition;
+ private final int spawnedEntityId;
+
+ public ClientboundSpawnerEventPacket(BlockPos spawnerPosition, int spawnedEntityId) {
+ this.spawnerPosition = spawnerPosition;
+ this.spawnedEntityId = spawnedEntityId;
+ }
+
+ public ClientboundSpawnerEventPacket(FriendlyByteBuf buffer) {
+ this(buffer.readBlockPos(), buffer.readInt());
+ }
+
+ public void encode(FriendlyByteBuf buffer) {
+ buffer.writeBlockPos(spawnerPosition);
+ buffer.writeInt(spawnedEntityId);
+ }
+
+ public void handle(Supplier ctx) {
+ ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
+ var level = Minecraft.getInstance().level;
+ if (level == null) return;
+
+ var entity = Minecraft.getInstance().level.getEntity(spawnedEntityId);
+ if (entity == null) return;
+
+ var entityBoundingBox = entity.getBoundingBox();
+ var entityCenter = entityBoundingBox.getCenter();
+ var spawnerCenter = spawnerPosition.getCenter();
+
+ ParticleUtils.drawParticleLine(ParticleTypes.WITCH, level, spawnerCenter, entityCenter, 0.5, Vec3.ZERO);
+ ParticleUtils.drawParticles(ParticleTypes.WITCH, level, entityCenter, ParticleUtils.getParticleCountForEntity(entity), entityBoundingBox.getXsize() / 3, entityBoundingBox.getYsize() / 3, entityBoundingBox.getZsize() / 3, Vec3.ZERO);
+ ParticleUtils.drawParticles(ParticleTypes.ENTITY_EFFECT, level, spawnerCenter, 40, 0.5, 0.5, 0.5, new Vec3(205 / 255.0, 92 / 255.0, 171 / 255.0));
+ }));
+ ctx.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/network/PacketHandler.java b/src/main/java/dev/kvnmtz/createmobspawners/network/PacketHandler.java
new file mode 100644
index 0000000..cef684b
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/network/PacketHandler.java
@@ -0,0 +1,50 @@
+package dev.kvnmtz.createmobspawners.network;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+import net.minecraftforge.network.NetworkDirection;
+import net.minecraftforge.network.NetworkRegistry;
+import net.minecraftforge.network.PacketDistributor;
+import net.minecraftforge.network.simple.SimpleChannel;
+
+public class PacketHandler {
+ private static final String PROTOCOL_VERSION = "1";
+ private static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(
+ CreateMobSpawners.asResource("main"),
+ () -> PROTOCOL_VERSION,
+ PROTOCOL_VERSION::equals,
+ PROTOCOL_VERSION::equals
+ );
+
+ private static int currentPacketId = 0;
+
+ public static void register() {
+ INSTANCE.messageBuilder(ClientboundSpawnerEventPacket.class, currentPacketId++, NetworkDirection.PLAY_TO_CLIENT)
+ .encoder(ClientboundSpawnerEventPacket::encode)
+ .decoder(ClientboundSpawnerEventPacket::new)
+ .consumerMainThread(ClientboundSpawnerEventPacket::handle)
+ .add();
+
+ INSTANCE.messageBuilder(ClientboundEntityCatchPacket.class, currentPacketId++, NetworkDirection.PLAY_TO_CLIENT)
+ .encoder(ClientboundEntityCatchPacket::encode)
+ .decoder(ClientboundEntityCatchPacket::new)
+ .consumerMainThread(ClientboundEntityCatchPacket::handle)
+ .add();
+
+ INSTANCE.messageBuilder(ClientboundEntityReleasePacket.class, currentPacketId++, NetworkDirection.PLAY_TO_CLIENT)
+ .encoder(ClientboundEntityReleasePacket::encode)
+ .decoder(ClientboundEntityReleasePacket::new)
+ .consumerMainThread(ClientboundEntityReleasePacket::handle)
+ .add();
+ }
+
+ public static void sendToNearbyPlayers(Object packet, Vec3 position, double radius, ResourceKey dimension) {
+ INSTANCE.send(PacketDistributor.NEAR.with(PacketDistributor.TargetPoint.p(position.x, position.y, position.z, radius, dimension)), packet);
+ }
+
+ public static void sendToAllPlayers(Object packet) {
+ INSTANCE.send(PacketDistributor.ALL.noArg(), packet);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/utils/BoundingBoxUtils.java b/src/main/java/dev/kvnmtz/createmobspawners/utils/BoundingBoxUtils.java
new file mode 100644
index 0000000..f4e24a8
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/utils/BoundingBoxUtils.java
@@ -0,0 +1,9 @@
+package dev.kvnmtz.createmobspawners.utils;
+
+import net.minecraft.world.phys.AABB;
+
+public class BoundingBoxUtils {
+ public static double getBoundingBoxVolume(AABB boundingBox) {
+ return Math.abs(boundingBox.getXsize() * boundingBox.getYsize() * boundingBox.getZsize());
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/utils/DropUtils.java b/src/main/java/dev/kvnmtz/createmobspawners/utils/DropUtils.java
new file mode 100644
index 0000000..f96379e
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/utils/DropUtils.java
@@ -0,0 +1,36 @@
+package dev.kvnmtz.createmobspawners.utils;
+
+import net.minecraft.world.Containers;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.Level;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DropUtils {
+ /**
+ * {@link Containers#dropItemStack(Level, double, double, double, ItemStack)}
+ * but with the dropped entities as return value
+ */
+ public static Set dropItemStack(Level level, double x, double y, double z, ItemStack itemStack) {
+ var $$5 = EntityType.ITEM.getWidth();
+ var $$6 = (double) 1.0F - $$5;
+ var $$7 = $$5 / (double) 2.0F;
+ var $$8 = Math.floor(x) + level.random.nextDouble() * $$6 + $$7;
+ var $$9 = Math.floor(y) + level.random.nextDouble() * $$6;
+ var $$10 = Math.floor(z) + level.random.nextDouble() * $$6 + $$7;
+
+ var droppedItemEntities = new HashSet();
+
+ while (!itemStack.isEmpty()) {
+ var $$11 = new ItemEntity(level, $$8, $$9, $$10, itemStack.split(level.random.nextInt(21) + 10));
+ $$11.setDeltaMovement(level.random.triangle(0.0F, 0.11485000171139836), level.random.triangle(0.2, 0.11485000171139836), level.random.triangle(0.0F, 0.11485000171139836));
+ level.addFreshEntity($$11);
+ droppedItemEntities.add($$11);
+ }
+
+ return droppedItemEntities;
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/utils/ParticleUtils.java b/src/main/java/dev/kvnmtz/createmobspawners/utils/ParticleUtils.java
new file mode 100644
index 0000000..2c8a830
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/utils/ParticleUtils.java
@@ -0,0 +1,50 @@
+package dev.kvnmtz.createmobspawners.utils;
+
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.core.particles.ParticleOptions;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.Vec3;
+
+public class ParticleUtils {
+
+ public static void drawParticleLine(ParticleOptions particleType, ClientLevel level, Vec3 pos1, Vec3 pos2, double space, Vec3 speed) {
+ var direction = pos2.subtract(pos1).normalize();
+ var distance = pos1.distanceTo(pos2);
+
+ var lastPosition = pos1;
+ for (double i = 0; i < distance; i += space) {
+ var position = lastPosition.add(direction.scale(space));
+ level.addParticle(particleType, position.x, position.y, position.z, speed.x, speed.y, speed.z);
+ lastPosition = position;
+ }
+ }
+
+ public static void drawParticlesWithRandomSpeed(ParticleOptions particleType, ClientLevel level, Vec3 position, int amount, double xOffset, double yOffset, double zOffset, double maxSpeed) {
+ var random = level.random;
+ for (var i = 0; i < amount; i++) {
+ var x = position.x + xOffset * random.nextGaussian();
+ var y = position.y + yOffset * random.nextGaussian();
+ var z = position.z + zOffset * random.nextGaussian();
+ var speedX = maxSpeed * random.nextGaussian();
+ var speedY = maxSpeed * random.nextGaussian();
+ var speedZ = maxSpeed * random.nextGaussian();
+ level.addParticle(particleType, x, y, z, speedX, speedY, speedZ);
+ }
+ }
+
+ public static void drawParticles(ParticleOptions particleType, ClientLevel level, Vec3 position, int amount, double xOffset, double yOffset, double zOffset, Vec3 speed) {
+ var random = level.random;
+ for (var i = 0; i < amount; i++) {
+ var x = position.x + xOffset * random.nextGaussian();
+ var y = position.y + yOffset * random.nextGaussian();
+ var z = position.z + zOffset * random.nextGaussian();
+ level.addParticle(particleType, x, y, z, speed.x, speed.y, speed.z);
+ }
+ }
+
+ public static int getParticleCountForEntity(Entity entity) {
+ var boundingBox = entity.getBoundingBox();
+ var volume = BoundingBoxUtils.getBoundingBoxVolume(boundingBox);
+ return (int) (24.58 * Math.pow(volume, 0.35));
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/waila/CreateMobSpawnersWailaPlugin.java b/src/main/java/dev/kvnmtz/createmobspawners/waila/CreateMobSpawnersWailaPlugin.java
new file mode 100644
index 0000000..100a269
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/waila/CreateMobSpawnersWailaPlugin.java
@@ -0,0 +1,21 @@
+package dev.kvnmtz.createmobspawners.waila;
+
+import dev.kvnmtz.createmobspawners.blocks.MechanicalSpawnerBlock;
+import dev.kvnmtz.createmobspawners.blocks.entity.MechanicalSpawnerBlockEntity;
+import snownee.jade.api.IWailaClientRegistration;
+import snownee.jade.api.IWailaCommonRegistration;
+import snownee.jade.api.IWailaPlugin;
+import snownee.jade.api.WailaPlugin;
+
+@WailaPlugin
+public class CreateMobSpawnersWailaPlugin implements IWailaPlugin {
+ @Override
+ public void register(IWailaCommonRegistration registration) {
+ registration.registerBlockDataProvider(MechanicalSpawnerComponentProvider.INSTANCE, MechanicalSpawnerBlockEntity.class);
+ }
+
+ @Override
+ public void registerClient(IWailaClientRegistration registration) {
+ registration.registerBlockComponent(MechanicalSpawnerComponentProvider.INSTANCE, MechanicalSpawnerBlock.class);
+ }
+}
diff --git a/src/main/java/dev/kvnmtz/createmobspawners/waila/MechanicalSpawnerComponentProvider.java b/src/main/java/dev/kvnmtz/createmobspawners/waila/MechanicalSpawnerComponentProvider.java
new file mode 100644
index 0000000..e32191c
--- /dev/null
+++ b/src/main/java/dev/kvnmtz/createmobspawners/waila/MechanicalSpawnerComponentProvider.java
@@ -0,0 +1,77 @@
+package dev.kvnmtz.createmobspawners.waila;
+
+import dev.kvnmtz.createmobspawners.CreateMobSpawners;
+import dev.kvnmtz.createmobspawners.blocks.entity.MechanicalSpawnerBlockEntity;
+import net.minecraft.ChatFormatting;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import snownee.jade.api.*;
+import snownee.jade.api.config.IPluginConfig;
+import snownee.jade.api.theme.IThemeHelper;
+
+public enum MechanicalSpawnerComponentProvider implements IBlockComponentProvider, IServerDataProvider {
+ INSTANCE;
+
+ @Override
+ public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
+ var spawner = (MechanicalSpawnerBlockEntity) accessor.getBlockEntity();
+ var entityData = spawner.getStoredEntityData();
+ if (!entityData.isEmpty()) {
+ var optEntityDisplayName = entityData.getEntityDisplayName();
+ if (optEntityDisplayName.isPresent()) {
+ var title = accessor.getBlock().getName();
+ title = Component.translatable("create_mob_spawners.waila.spawner_title", title, optEntityDisplayName.get());
+ tooltip.remove(Identifiers.CORE_OBJECT_NAME);
+ tooltip.add(0, IThemeHelper.get().title(title), Identifiers.CORE_OBJECT_NAME);
+
+ var serverData = accessor.getServerData();
+ if (serverData.contains("NoProgressReason")) {
+ tooltip.add(Component.translatable("create_mob_spawners.waila.spawner_no_progress_reason." + serverData.getString("NoProgressReason")).withStyle(ChatFormatting.RED));
+ } else if (serverData.contains("Progress")) {
+ tooltip.add(
+ Component.translatable(
+ "create_mob_spawners.waila.spawner_progress",
+ IThemeHelper.get().info(String.format("%d%%", serverData.getInt("Progress")))
+ )
+ );
+ } else if (serverData.contains("DelayReason")) {
+ tooltip.add(
+ Component.translatable(
+ "create_mob_spawners.waila.spawner_progress",
+ IThemeHelper.get().warning(Component.translatable("create_mob_spawners.waila.spawner_progress.delaying"))
+ )
+ );
+ tooltip.add(
+ Component.translatable(
+ "create_mob_spawners.waila.spawner_progress.delay_reason",
+ IThemeHelper.get().info(Component.translatable(serverData.getString("DelayReason")))
+ )
+ );
+ }
+
+ return;
+ }
+ }
+
+ tooltip.add(Component.translatable("create_mob_spawners.waila.spawner_no_progress_reason.no_soul").withStyle(ChatFormatting.RED));
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return CreateMobSpawners.asResource("spawner_progress");
+ }
+
+ @Override
+ public void appendServerData(CompoundTag data, BlockAccessor accessor) {
+ var spawner = (MechanicalSpawnerBlockEntity) accessor.getBlockEntity();
+ var optReasonForNotProgressingKey = spawner.getReasonForNotProgressingTranslationKey();
+ if (optReasonForNotProgressingKey.isPresent()) {
+ data.putString("NoProgressReason", optReasonForNotProgressingKey.get());
+ } else if (spawner.isDelayed()) {
+ data.putString("DelayReason", spawner.getDelayReasonTranslationKey());
+ } else {
+ data.putInt("Progress", spawner.getSpawnProgressPercentage());
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml
new file mode 100644
index 0000000..472c3b7
--- /dev/null
+++ b/src/main/resources/META-INF/mods.toml
@@ -0,0 +1,77 @@
+# This is an example mods.toml file. It contains the data relating to the loading mods.
+# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
+# The overall format is standard TOML format, v0.5.0.
+# Note that there are a couple of TOML lists in this file.
+# Find more information on toml format here: https://github.com/toml-lang/toml
+# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
+modLoader="javafml" #mandatory
+# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
+loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
+# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
+# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
+license="${mod_license}"
+# A URL to refer people to when problems occur with this mod
+#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
+# A list of mods - how many allowed here is determined by the individual mod loader
+[[mods]] #mandatory
+# The modid of the mod
+modId="${mod_id}" #mandatory
+# The version number of the mod
+version="${mod_version}" #mandatory
+# A display name for the mod
+displayName="${mod_name}" #mandatory
+# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
+#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
+# A URL for the "homepage" for this mod, displayed in the mod UI
+#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
+# A file name (in the root of the mod JAR) containing a logo for display
+logoFile="icon.png"
+# A text field displayed in the mod UI
+#credits="" #optional
+# A text field displayed in the mod UI
+authors="${mod_authors}" #optional
+# Display Test controls the display for your mod in the server connection screen
+# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
+# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
+# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
+# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
+# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
+#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional)
+
+# The description text for the mod (multi line!) (#mandatory)
+description='''${mod_description}'''
+# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
+[[dependencies.${mod_id}]] #optional
+ # the modid of the dependency
+ modId="forge" #mandatory
+ # Does this dependency have to exist - if not, ordering below must be specified
+ mandatory=true #mandatory
+ # The version range of the dependency
+ versionRange="${forge_version_range}" #mandatory
+ # An ordering relationship for the dependency - BEFORE or AFTER required if the dependency is not mandatory
+ # BEFORE - This mod is loaded BEFORE the dependency
+ # AFTER - This mod is loaded AFTER the dependency
+ ordering="NONE"
+ # Side this dependency is applied on - BOTH, CLIENT, or SERVER
+ side="BOTH"
+# Here's another dependency
+[[dependencies.${mod_id}]]
+ modId="minecraft"
+ mandatory=true
+ # This version range declares a minimum of the current minecraft version up to but not including the next major version
+ versionRange="${minecraft_version_range}"
+ ordering="NONE"
+ side="BOTH"
+
+[[dependencies.${mod_id}]]
+ modId="create"
+ mandatory=true
+ versionRange="[0.5.1.j,)"
+ ordering="AFTER"
+ side="BOTH"
+
+# Features are specific properties of the game environment, that you may want to declare you require. This example declares
+# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
+# stop your mod loading on the server for example.
+#[features.${mod_id}]
+#openGLVersion="[3.2,)"
\ No newline at end of file
diff --git a/src/main/resources/assets/.gitignore b/src/main/resources/assets/.gitignore
new file mode 100644
index 0000000..4c3b1a7
--- /dev/null
+++ b/src/main/resources/assets/.gitignore
@@ -0,0 +1,2 @@
+create/
+minecraft/
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/blockstates/mechanical_spawner.json b/src/main/resources/assets/create_mob_spawners/blockstates/mechanical_spawner.json
new file mode 100644
index 0000000..ac63241
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/blockstates/mechanical_spawner.json
@@ -0,0 +1,19 @@
+{
+ "variants": {
+ "facing=north": {
+ "model": "create_mob_spawners:block/mechanical_spawner"
+ },
+ "facing=east": {
+ "model": "create_mob_spawners:block/mechanical_spawner",
+ "y": 90
+ },
+ "facing=south": {
+ "model": "create_mob_spawners:block/mechanical_spawner",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "create_mob_spawners:block/mechanical_spawner",
+ "y": 270
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/lang/de_de.json b/src/main/resources/assets/create_mob_spawners/lang/de_de.json
new file mode 100644
index 0000000..1bb3041
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/lang/de_de.json
@@ -0,0 +1,36 @@
+{
+ "item.create_mob_spawners.empty_soul_catcher": "Leerer Seelenfänger",
+ "item.create_mob_spawners.empty_soul_catcher.tooltip.summary": "Nutzt die Kraft kondensierter Seelen, um Kreaturen zu fangen. Rechtsklicke eine Kreatur mit einem _Schwächeeffekt_, um sie _einzufangen_. Sie kann später mit einem Rechtsklick auf die gewünschte Position wieder _freigelassen_ werden.",
+
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.too_far": "Kreatur ist zu weit weg",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.dead": "Tote Kreaturen können nicht gefangen werden",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.no_weakness": "Kreatur ist nicht geschwächt. Versuche es mit einem Wurftrank der Schwäche.",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.player": "Spieler können nicht gefangen werden",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.boss": "Bosse können nicht gefangen werden",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.blacklisted": "Diese Kreatur kann nicht gefangen werden",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.incompatible": "Diese Kreatur kann nicht gefangen werden",
+
+ "item.create_mob_spawners.soul_catcher": "Seelenfänger",
+ "item.create_mob_spawners.soul_catcher.tooltip.summary": "Enthält die Seele einer Kreatur. Sie kann entweder durch einen Rechtsklick auf die gewünschte Position _freigelassen_ oder in einem _Mechanischen Spawner_ platziert werden.",
+
+ "block.create_mob_spawners.mechanical_spawner": "Mechanischer Spawner",
+ "block.create_mob_spawners.mechanical_spawner.tooltip.summary": "Nutzt die Kraft regenerativer Magie um Kreaturen zu erschaffen. Benötigt _Rotationsenergie_, muss mit _Trank der Regeneration_ betankt werden und ein _Seelenfänger_ muss darin platziert werden, um zu funktionieren.",
+
+ "creativetab.create_mob_spawners_tab": "Create: Mob Spawners",
+
+ "config.jade.plugin_create_mob_spawners.spawner_progress": "Mechanischer Spawner Fortschritt",
+
+ "create_mob_spawners.waila.spawner_progress": "Fortschritt: %s",
+ "create_mob_spawners.waila.spawner_progress.delaying": "Verzögert...",
+ "create_mob_spawners.waila.spawner_progress.delay_reason": "Grund: %s",
+ "create_mob_spawners.waila.spawner_delay_reason.unknown": "Unbekannt",
+ "create_mob_spawners.waila.spawner_delay_reason.invalid_entity": "Unzulässige Kreatur enthalten",
+ "create_mob_spawners.waila.spawner_delay_reason.searching_position": "Suche zulässige Position...",
+ "create_mob_spawners.waila.spawner_delay_reason.entity_creation_error": "Konnte Kreatur nicht erschaffen",
+ "create_mob_spawners.waila.spawner_delay_reason.too_many_entities": "Zu viele Kreaturen in der Nähe",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_soul": "Keine Seele enthalten",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_regeneration_potion_liquid": "Keine \"Trank der Regeneration\"-Flüssigkeit",
+ "create_mob_spawners.waila.spawner_no_progress_reason.not_enough_regeneration_potion_liquid": "Nicht genug \"Trank der Regeneration\"-Flüssigkeit",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_rotational_force": "Keine Rotationsenergie",
+ "create_mob_spawners.waila.spawner_title": "%s (%s)"
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/lang/en_us.json b/src/main/resources/assets/create_mob_spawners/lang/en_us.json
new file mode 100644
index 0000000..af368df
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/lang/en_us.json
@@ -0,0 +1,36 @@
+{
+ "item.create_mob_spawners.empty_soul_catcher": "Empty Soul Catcher",
+ "item.create_mob_spawners.empty_soul_catcher.tooltip.summary": "Utilizes the power of condensed souls to capture mobs. Right-clicking a mob that has a _weakness_ potion effect will _capture_ its soul and store it inside. It can later be _released_ by right-clicking the desired position.",
+
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.too_far": "Entity is too far away",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.dead": "Dead entities are not catchable",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.no_weakness": "Entity is not weakened. Try applying a potion of weakness on it.",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.player": "Players cannot be captured",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.boss": "Bosses cannot be captured",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.blacklisted": "This entity is not catchable",
+ "item.create_mob_spawners.empty_soul_catcher.capturable_status.incompatible": "This entity is not catchable",
+
+ "item.create_mob_spawners.soul_catcher": "Soul Catcher",
+ "item.create_mob_spawners.soul_catcher.tooltip.summary": "Contains a mob's soul. It can either be _released_ by right-clicking the desired position or placed inside a _Mechanical Spawner_.",
+
+ "block.create_mob_spawners.mechanical_spawner": "Mechanical Spawner",
+ "block.create_mob_spawners.mechanical_spawner.tooltip.summary": "Utilizes the power of regenerative magic to spawn mobs. It needs _rotational force_, a supply of _Potion of Regeneration_ liquid and a _Soul Catcher_ placed inside to function.",
+
+ "creativetab.create_mob_spawners_tab": "Create: Mob Spawners",
+
+ "config.jade.plugin_create_mob_spawners.spawner_progress": "Mechanical Spawner Progress",
+
+ "create_mob_spawners.waila.spawner_progress": "Progress: %s",
+ "create_mob_spawners.waila.spawner_progress.delaying": "Delaying...",
+ "create_mob_spawners.waila.spawner_progress.delay_reason": "Reason: %s",
+ "create_mob_spawners.waila.spawner_delay_reason.unknown": "Unknown",
+ "create_mob_spawners.waila.spawner_delay_reason.invalid_entity": "Invalid entity contained",
+ "create_mob_spawners.waila.spawner_delay_reason.searching_position": "Searching for valid position...",
+ "create_mob_spawners.waila.spawner_delay_reason.entity_creation_error": "Could not create entity",
+ "create_mob_spawners.waila.spawner_delay_reason.too_many_entities": "Too many entities nearby",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_soul": "No soul stored inside",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_regeneration_potion_liquid": "No Potion of Regeneration liquid",
+ "create_mob_spawners.waila.spawner_no_progress_reason.not_enough_regeneration_potion_liquid": "Not enough Potion of Regeneration liquid",
+ "create_mob_spawners.waila.spawner_no_progress_reason.no_rotational_force": "No rotational force",
+ "create_mob_spawners.waila.spawner_title": "%s (%s)"
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/block/mechanical_spawner.json b/src/main/resources/assets/create_mob_spawners/models/block/mechanical_spawner.json
new file mode 100644
index 0000000..5e07289
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/block/mechanical_spawner.json
@@ -0,0 +1,984 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "render_type": "minecraft:translucent",
+ "textures": {
+ "1": "create:block/brass_gearbox",
+ "2": "create_mob_spawners:block/mechanical_spawner",
+ "3": "create:block/brass_casing",
+ "5": "create:block/spout",
+ "7": "block/spawner",
+ "9": "create_mob_spawners:block/mechanical_spawner_vertical_inside",
+ "particle": "create:block/brass_casing"
+ },
+ "elements": [
+ {
+ "from": [0, 0, 0],
+ "to": [16, 2, 2],
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "up": {"uv": [0, 0, 16, 2], "texture": "#3"},
+ "down": {"uv": [0, 14, 16, 16], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 0, 2],
+ "to": [2, 2, 14],
+ "faces": {
+ "north": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [0, 2, 2, 14], "texture": "#3"},
+ "down": {"uv": [0, 2, 2, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [2, 1, 2],
+ "to": [14, 2, 14],
+ "faces": {
+ "north": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [2, 2, 14, 14], "texture": "#9"},
+ "down": {"uv": [2, 2, 14, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [14, 0, 2],
+ "to": [16, 2, 14],
+ "faces": {
+ "north": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [14, 2, 16, 14], "texture": "#3"},
+ "down": {"uv": [14, 2, 16, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 0, 14],
+ "to": [16, 2, 16],
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "up": {"uv": [0, 14, 16, 16], "texture": "#3"},
+ "down": {"uv": [0, 0, 16, 2], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 14, 0],
+ "to": [16, 16, 2],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [16, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [16, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 14, 16, 16], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 0, 16, 2], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [14, 14, 2],
+ "to": [16, 16, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [14, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [0, 2, 2, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 2, 2, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 2, 2, 14], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [2, 14, 2],
+ "to": [14, 15, 14],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#9"}
+ }
+ },
+ {
+ "from": [0, 14, 2],
+ "to": [2, 16, 14],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 2, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [14, 2, 16, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [14, 2, 16, 14], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [0, 14, 14],
+ "to": [16, 16, 16],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [16, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [16, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 0, 16, 2], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 14, 16, 16], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [0, 2, 0],
+ "to": [2, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]},
+ "faces": {
+ "north": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "east": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "south": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "west": {"uv": [0, 2, 2, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [14, 2, 0],
+ "to": [16, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 1, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "east": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "south": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "west": {"uv": [0, 1, 2, 13], "texture": "#2"}
+ }
+ },
+ {
+ "from": [14, 2, 14],
+ "to": [16, 14, 16],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 1, 16]},
+ "faces": {
+ "north": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "east": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "west": {"uv": [0, 1, 2, 13], "texture": "#2"}
+ }
+ },
+ {
+ "from": [0, 2, 14],
+ "to": [2, 14, 16],
+ "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 16]},
+ "faces": {
+ "north": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "east": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "south": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "west": {"uv": [14, 2, 16, 14], "texture": "#1"}
+ }
+ },
+ {
+ "name": "fluid port",
+ "from": [2, 2, 14],
+ "to": [14, 14, 15],
+ "rotation": {"angle": 0, "axis": "y", "origin": [3, 3, 14]},
+ "faces": {
+ "north": {"uv": [1, 9, 7, 15], "texture": "#5"},
+ "south": {"uv": [1, 9, 7, 15], "texture": "#5"}
+ }
+ },
+ {
+ "from": [13, 2, 1],
+ "to": [14, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "south": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [2, 2, 1],
+ "to": [3, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "south": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [5, 6, 1],
+ "to": [11, 10, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "south": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "west": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 4, 1],
+ "to": [10, 6, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "south": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "west": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [10, 4, 1],
+ "to": [13, 5, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "south": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 4, 1],
+ "to": [6, 5, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "south": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [11, 8, 1],
+ "to": [13, 9, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "south": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 8, 1],
+ "to": [5, 9, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "south": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "texture": "#7"}
+ }
+ },
+ {
+ "from": [12, 10, 1],
+ "to": [13, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "south": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 10, 1],
+ "to": [4, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "south": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [12, 6, 1],
+ "to": [13, 7, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "south": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 6, 1],
+ "to": [4, 7, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "south": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [4, 12, 1],
+ "to": [12, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "south": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 13, 1],
+ "to": [10, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 13, 1],
+ "to": [7, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 13, 1],
+ "to": [10, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 10, 1],
+ "to": [10, 12, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 10, 1],
+ "to": [7, 12, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 2, 1],
+ "to": [11, 3, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "south": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "west": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [5, 2, 1],
+ "to": [7, 3, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "south": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "west": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 3, 1],
+ "to": [10, 4, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 3, 1],
+ "to": [7, 4, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 2],
+ "to": [2, 14, 3],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "south": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 13],
+ "to": [2, 14, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "west": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 5],
+ "to": [2, 10, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "south": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "west": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 6],
+ "to": [2, 6, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "south": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "west": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 3],
+ "to": [2, 5, 6],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "west": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 10],
+ "to": [2, 5, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "west": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 8, 3],
+ "to": [2, 9, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "west": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 8, 11],
+ "to": [2, 9, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "west": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 3],
+ "to": [2, 13, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "south": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 12],
+ "to": [2, 13, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "west": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 3],
+ "to": [2, 7, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "south": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 12],
+ "to": [2, 7, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "west": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 12, 4],
+ "to": [2, 13, 12],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "west": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 6],
+ "to": [2, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 9],
+ "to": [2, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 6],
+ "to": [2, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 6],
+ "to": [2, 12, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 9],
+ "to": [2, 12, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 5],
+ "to": [2, 3, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "south": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "west": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 9],
+ "to": [2, 3, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "south": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "west": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 3, 6],
+ "to": [2, 4, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 3, 9],
+ "to": [2, 4, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 13],
+ "to": [15, 14, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "east": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 2],
+ "to": [15, 14, 3],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "south": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "west": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 5],
+ "to": [15, 10, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "south": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "west": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 6],
+ "to": [15, 6, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "south": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "west": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 10],
+ "to": [15, 5, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "west": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 3],
+ "to": [15, 5, 6],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "west": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 8, 11],
+ "to": [15, 9, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "west": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 8, 3],
+ "to": [15, 9, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "west": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 12],
+ "to": [15, 13, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "east": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 3],
+ "to": [15, 13, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "south": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "west": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 12],
+ "to": [15, 7, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "east": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 3],
+ "to": [15, 7, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "south": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "west": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 12, 4],
+ "to": [15, 13, 12],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "west": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 9],
+ "to": [15, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 6],
+ "to": [15, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 9],
+ "to": [15, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 9],
+ "to": [15, 12, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 6],
+ "to": [15, 12, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 9],
+ "to": [15, 3, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "south": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "west": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 5],
+ "to": [15, 3, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "south": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "west": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 3, 9],
+ "to": [15, 4, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 3, 6],
+ "to": [15, 4, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "sheet bottom",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [0, 1, 2, 3, 4]
+ },
+ {
+ "name": "sheet top",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [5, 6, 7, 8, 9]
+ },
+ {
+ "name": "pillars",
+ "origin": [14, 1, 0],
+ "color": 0,
+ "children": [10, 11, 12, 13]
+ },
+ 14,
+ {
+ "name": "bars north",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]
+ },
+ {
+ "name": "bars west",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]
+ },
+ {
+ "name": "bars east",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/item/empty_soul_catcher.json b/src/main/resources/assets/create_mob_spawners/models/item/empty_soul_catcher.json
new file mode 100644
index 0000000..e000435
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/item/empty_soul_catcher.json
@@ -0,0 +1,235 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "textures": {
+ "0": "create_mob_spawners:item/soul_catcher"
+ },
+ "elements": [
+ {
+ "name": "right",
+ "from": [4, 5, 12],
+ "to": [5, 13, 15],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [9, 6, 10, 14], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 13, 8, 10], "rotation": 270, "texture": "#0"},
+ "south": {"uv": [1, 8, 0, 0], "texture": "#0"},
+ "west": {"uv": [0, 10, 8, 13], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [0, 10, 1, 13], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [7, 10, 8, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "left",
+ "from": [11, 5, 12],
+ "to": [12, 13, 15],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [9, 6, 10, 14], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 13, 8, 10], "rotation": 270, "texture": "#0"},
+ "south": {"uv": [1, 8, 0, 0], "texture": "#0"},
+ "west": {"uv": [0, 10, 8, 13], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [0, 10, 1, 13], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [7, 10, 8, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "bottom",
+ "from": [4, 6, 15],
+ "to": [12, 12, 16],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "east": {"uv": [1, 8, 7, 9], "rotation": 270, "texture": "#0"},
+ "south": {"uv": [16, 13, 8, 7], "texture": "#0"},
+ "west": {"uv": [1, 4, 7, 5], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [0, 4, 8, 5], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [0, 8, 8, 9], "texture": "#0"}
+ }
+ },
+ {
+ "name": "front",
+ "from": [5, 12, 12],
+ "to": [11, 13, 16],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [1, 12, 7, 13], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [7, 9, 8, 13], "rotation": 270, "texture": "#0"},
+ "south": {"uv": [7, 10, 1, 9], "texture": "#0"},
+ "west": {"uv": [0, 9, 1, 13], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [1, 9, 7, 13], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 9, 7, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "back",
+ "from": [5, 5, 12],
+ "to": [11, 6, 16],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [1, 12, 7, 13], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [7, 9, 8, 13], "rotation": 270, "texture": "#0"},
+ "south": {"uv": [7, 10, 1, 9], "texture": "#0"},
+ "west": {"uv": [0, 9, 1, 13], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [1, 9, 7, 13], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 9, 7, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "inside",
+ "from": [5, 6, 14],
+ "to": [11, 12, 14],
+ "rotation": {"angle": -45, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [16, 0, 10, 6], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "left",
+ "from": [11, 1, 4],
+ "to": [12, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 1, 8], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [7, 4, 8, 8], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [0, 0, 1, 8], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 8, 0, 0], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "right",
+ "from": [4, 1, 4],
+ "to": [5, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [1, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 1, 8], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [7, 4, 8, 8], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [0, 0, 1, 8], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 8, 0, 0], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "front",
+ "from": [5, 0, 4],
+ "to": [11, 4, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [1, 4, 7, 0], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 0, 1, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [1, 4, 7, 0], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [7, 0, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [1, 0, 7, 1], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 3, 7, 4], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "front_top_left",
+ "from": [9, 4, 4],
+ "to": [11, 5, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [9, 4, 4]},
+ "faces": {
+ "north": {"uv": [4, 3, 6, 4], "texture": "#0"},
+ "south": {"uv": [2, 3, 4, 4], "texture": "#0"},
+ "west": {"uv": [0, 0, 1, 1], "texture": "#0"},
+ "up": {"uv": [2, 3, 4, 4], "texture": "#0"}
+ }
+ },
+ {
+ "name": "front_top_right",
+ "from": [5, 4, 4],
+ "to": [7, 5, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [5, 4, 4]},
+ "faces": {
+ "north": {"uv": [4, 3, 6, 4], "texture": "#0"},
+ "east": {"uv": [0, 0, 1, 1], "texture": "#0"},
+ "south": {"uv": [2, 3, 4, 4], "texture": "#0"},
+ "up": {"uv": [2, 3, 4, 4], "texture": "#0"}
+ }
+ },
+ {
+ "name": "bottom",
+ "from": [4, 0, 5],
+ "to": [12, 1, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 8, 5], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [1, 4, 7, 5], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [0, 8, 8, 9], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [1, 8, 7, 9], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [16, 13, 8, 7], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "back",
+ "from": [5, 0, 11],
+ "to": [11, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [1, 4, 7, 9], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 4, 1, 9], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [1, 4, 7, 9], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [7, 4, 8, 9], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [1, 4, 7, 5], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [7, 5, 1, 4], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "inside",
+ "from": [5, 2, 5],
+ "to": [11, 2, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "up": {"uv": [16, 0, 10, 6], "rotation": 180, "texture": "#0"}
+ }
+ }
+ ],
+ "display": {
+ "thirdperson_righthand": {
+ "rotation": [51, 0, 0],
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "thirdperson_lefthand": {
+ "rotation": [51, 0, 0],
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "firstperson_righthand": {
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "firstperson_lefthand": {
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "ground": {
+ "translation": [0, 2.75, 0],
+ "scale": [0.35, 0.35, 0.35]
+ },
+ "gui": {
+ "rotation": [25, -145, 0],
+ "translation": [0, 1.5, 0],
+ "scale": [0.9, 0.9, 0.9]
+ },
+ "fixed": {
+ "translation": [0, 2.25, -4.25]
+ }
+ },
+ "groups": [
+ {
+ "name": "upper half",
+ "origin": [7, 3, 10],
+ "color": 0,
+ "children": [0, 1, 2, 3, 4, 5]
+ },
+ {
+ "name": "lower half",
+ "origin": [7, 3, 10],
+ "color": 0,
+ "children": [6, 7, 8, 9, 10, 11, 12, 13]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/item/mechanical_spawner.json b/src/main/resources/assets/create_mob_spawners/models/item/mechanical_spawner.json
new file mode 100644
index 0000000..9f0ace6
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/item/mechanical_spawner.json
@@ -0,0 +1,1000 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "render_type": "minecraft:translucent",
+ "textures": {
+ "1": "create:block/brass_gearbox",
+ "2": "create_mob_spawners:block/mechanical_spawner",
+ "3": "create:block/brass_casing",
+ "5": "create:block/spout",
+ "6": "create:block/axis_top",
+ "7": "block/spawner",
+ "8": "create:block/axis",
+ "9": "create_mob_spawners:block/mechanical_spawner_vertical_inside",
+ "particle": "create:block/brass_casing"
+ },
+ "elements": [
+ {
+ "from": [0, 0, 0],
+ "to": [16, 2, 2],
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "up": {"uv": [0, 0, 16, 2], "texture": "#3"},
+ "down": {"uv": [0, 14, 16, 16], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 0, 2],
+ "to": [2, 2, 14],
+ "faces": {
+ "north": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [0, 2, 2, 14], "texture": "#3"},
+ "down": {"uv": [0, 2, 2, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [2, 1, 2],
+ "to": [14, 2, 14],
+ "faces": {
+ "north": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [2, 2, 14, 14], "texture": "#9"},
+ "down": {"uv": [2, 2, 14, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [14, 0, 2],
+ "to": [16, 2, 14],
+ "faces": {
+ "north": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "texture": "#1"},
+ "up": {"uv": [14, 2, 16, 14], "texture": "#3"},
+ "down": {"uv": [14, 2, 16, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 0, 14],
+ "to": [16, 2, 16],
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "east": {"uv": [0, 2, 2, 0], "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "texture": "#1"},
+ "west": {"uv": [14, 2, 16, 0], "texture": "#1"},
+ "up": {"uv": [0, 14, 16, 16], "texture": "#3"},
+ "down": {"uv": [0, 0, 16, 2], "texture": "#1"}
+ }
+ },
+ {
+ "from": [0, 14, 0],
+ "to": [16, 16, 2],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [16, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [16, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [0, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 14, 16, 16], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 0, 16, 2], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [14, 14, 2],
+ "to": [16, 16, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [14, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [0, 2, 2, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 2, 2, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 2, 2, 14], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [2, 14, 2],
+ "to": [14, 15, 14],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#9"}
+ }
+ },
+ {
+ "from": [0, 14, 2],
+ "to": [2, 16, 14],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 2, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [2, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [14, 2, 16, 14], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [14, 2, 16, 14], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [0, 14, 14],
+ "to": [16, 16, 16],
+ "rotation": {"angle": 0, "axis": "z", "origin": [16, 16, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 16, 0], "rotation": 180, "texture": "#1"},
+ "east": {"uv": [2, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "south": {"uv": [16, 2, 0, 0], "rotation": 180, "texture": "#1"},
+ "west": {"uv": [16, 2, 14, 0], "rotation": 180, "texture": "#1"},
+ "up": {"uv": [0, 0, 16, 2], "rotation": 180, "texture": "#1"},
+ "down": {"uv": [0, 14, 16, 16], "rotation": 180, "texture": "#3"}
+ }
+ },
+ {
+ "from": [0, 2, 0],
+ "to": [2, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 0]},
+ "faces": {
+ "north": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "east": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "south": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "west": {"uv": [0, 2, 2, 14], "texture": "#1"}
+ }
+ },
+ {
+ "from": [14, 2, 0],
+ "to": [16, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 1, 0]},
+ "faces": {
+ "north": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "east": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "south": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "west": {"uv": [0, 1, 2, 13], "texture": "#2"}
+ }
+ },
+ {
+ "from": [14, 2, 14],
+ "to": [16, 14, 16],
+ "rotation": {"angle": 0, "axis": "y", "origin": [16, 1, 16]},
+ "faces": {
+ "north": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "east": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "south": {"uv": [14, 2, 16, 14], "texture": "#1"},
+ "west": {"uv": [0, 1, 2, 13], "texture": "#2"}
+ }
+ },
+ {
+ "from": [0, 2, 14],
+ "to": [2, 14, 16],
+ "rotation": {"angle": 0, "axis": "y", "origin": [0, 1, 16]},
+ "faces": {
+ "north": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "east": {"uv": [0, 1, 2, 13], "texture": "#2"},
+ "south": {"uv": [0, 2, 2, 14], "texture": "#1"},
+ "west": {"uv": [14, 2, 16, 14], "texture": "#1"}
+ }
+ },
+ {
+ "name": "fluid port",
+ "from": [2, 2, 14],
+ "to": [14, 14, 15],
+ "rotation": {"angle": 0, "axis": "y", "origin": [3, 3, 14]},
+ "faces": {
+ "north": {"uv": [1, 9, 7, 15], "texture": "#5"},
+ "south": {"uv": [1, 9, 7, 15], "texture": "#5"}
+ }
+ },
+ {
+ "from": [13, 2, 1],
+ "to": [14, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "south": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [2, 2, 1],
+ "to": [3, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "south": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [5, 6, 1],
+ "to": [11, 10, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "south": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "west": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 4, 1],
+ "to": [10, 6, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "south": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "west": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [10, 4, 1],
+ "to": [13, 5, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "south": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 4, 1],
+ "to": [6, 5, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "south": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "texture": "#7"}
+ }
+ },
+ {
+ "from": [11, 8, 1],
+ "to": [13, 9, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "south": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 8, 1],
+ "to": [5, 9, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "south": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "texture": "#7"}
+ }
+ },
+ {
+ "from": [12, 10, 1],
+ "to": [13, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "south": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 10, 1],
+ "to": [4, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "south": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [12, 6, 1],
+ "to": [13, 7, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "south": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [3, 6, 1],
+ "to": [4, 7, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "south": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "texture": "#7"}
+ }
+ },
+ {
+ "from": [4, 12, 1],
+ "to": [12, 13, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "south": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 13, 1],
+ "to": [10, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 13, 1],
+ "to": [7, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 13, 1],
+ "to": [10, 14, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 10, 1],
+ "to": [10, 12, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 10, 1],
+ "to": [7, 12, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 2, 1],
+ "to": [11, 3, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "south": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "west": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [5, 2, 1],
+ "to": [7, 3, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "south": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "west": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [9, 3, 1],
+ "to": [10, 4, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [6, 3, 1],
+ "to": [7, 4, 2],
+ "rotation": {"angle": 0, "axis": "y", "origin": [2, 2, 1]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 2],
+ "to": [2, 14, 3],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "south": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 13],
+ "to": [2, 14, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "west": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 5],
+ "to": [2, 10, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "south": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "west": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 6],
+ "to": [2, 6, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "south": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "west": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 3],
+ "to": [2, 5, 6],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "west": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 4, 10],
+ "to": [2, 5, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "west": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 8, 3],
+ "to": [2, 9, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "west": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 8, 11],
+ "to": [2, 9, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "west": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 3],
+ "to": [2, 13, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "south": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 12],
+ "to": [2, 13, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "west": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 3],
+ "to": [2, 7, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "south": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 6, 12],
+ "to": [2, 7, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "west": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 12, 4],
+ "to": [2, 13, 12],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "east": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "west": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "rotation": 270, "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 6],
+ "to": [2, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 9],
+ "to": [2, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 13, 6],
+ "to": [2, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 6],
+ "to": [2, 12, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 10, 9],
+ "to": [2, 12, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 5],
+ "to": [2, 3, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "south": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "west": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 2, 9],
+ "to": [2, 3, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "south": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "west": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 3, 6],
+ "to": [2, 4, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [1, 3, 9],
+ "to": [2, 4, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [1, 2, 14]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 13],
+ "to": [15, 14, 14],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "east": {"uv": [2, 2, 3, 14], "texture": "#7"},
+ "west": {"uv": [2, 2, 3, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 2],
+ "to": [15, 14, 3],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "south": {"uv": [13, 2, 14, 14], "texture": "#7"},
+ "west": {"uv": [13, 2, 14, 14], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 5],
+ "to": [15, 10, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [10, 6, 11, 10], "texture": "#7"},
+ "east": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "south": {"uv": [5, 6, 6, 10], "texture": "#7"},
+ "west": {"uv": [5, 6, 11, 10], "texture": "#7"},
+ "up": {"uv": [5, 6, 11, 7], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [5, 9, 11, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 6],
+ "to": [15, 6, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 10, 10, 12], "texture": "#7"},
+ "east": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "south": {"uv": [6, 10, 7, 12], "texture": "#7"},
+ "west": {"uv": [6, 10, 10, 12], "texture": "#7"},
+ "up": {"uv": [6, 10, 10, 11], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [6, 11, 10, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 10],
+ "to": [15, 5, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "west": {"uv": [3, 11, 6, 12], "texture": "#7"},
+ "up": {"uv": [3, 11, 6, 12], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 11, 6, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 4, 3],
+ "to": [15, 5, 6],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "west": {"uv": [10, 11, 13, 12], "texture": "#7"},
+ "up": {"uv": [10, 11, 13, 12], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [10, 11, 13, 12], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 8, 11],
+ "to": [15, 9, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "west": {"uv": [3, 7, 5, 8], "texture": "#7"},
+ "up": {"uv": [3, 7, 5, 8], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 7, 5, 8], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 8, 3],
+ "to": [15, 9, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "west": {"uv": [11, 7, 13, 8], "texture": "#7"},
+ "up": {"uv": [11, 7, 13, 8], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [11, 7, 13, 8], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 12],
+ "to": [15, 13, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "east": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "west": {"uv": [3, 3, 4, 6], "texture": "#7"},
+ "up": {"uv": [3, 3, 4, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 5, 4, 6], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 3],
+ "to": [15, 13, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "south": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "west": {"uv": [12, 3, 13, 6], "texture": "#7"},
+ "up": {"uv": [12, 3, 13, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [12, 5, 13, 6], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 12],
+ "to": [15, 7, 13],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "east": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "west": {"uv": [3, 9, 4, 10], "texture": "#7"},
+ "up": {"uv": [3, 9, 4, 10], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [3, 9, 4, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 6, 3],
+ "to": [15, 7, 4],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "south": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "west": {"uv": [12, 9, 13, 10], "texture": "#7"},
+ "up": {"uv": [12, 9, 13, 10], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [12, 9, 13, 10], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 12, 4],
+ "to": [15, 13, 12],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "east": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "west": {"uv": [4, 3, 12, 4], "texture": "#7"},
+ "up": {"uv": [4, 3, 12, 4], "rotation": 90, "texture": "#7"},
+ "down": {"uv": [4, 3, 12, 4], "rotation": 270, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 9],
+ "to": [15, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 6],
+ "to": [15, 14, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "east": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "south": {"uv": [9, 2, 10, 3], "texture": "#7"},
+ "west": {"uv": [9, 2, 10, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 13, 9],
+ "to": [15, 14, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "east": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "south": {"uv": [6, 2, 7, 3], "texture": "#7"},
+ "west": {"uv": [6, 2, 7, 3], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 9],
+ "to": [15, 12, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "east": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "south": {"uv": [6, 4, 7, 6], "texture": "#7"},
+ "west": {"uv": [6, 4, 7, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 10, 6],
+ "to": [15, 12, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "east": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "south": {"uv": [9, 4, 10, 6], "texture": "#7"},
+ "west": {"uv": [9, 4, 10, 6], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 9],
+ "to": [15, 3, 11],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 13, 7, 14], "texture": "#7"},
+ "east": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "south": {"uv": [5, 13, 6, 14], "texture": "#7"},
+ "west": {"uv": [5, 13, 7, 14], "texture": "#7"},
+ "up": {"uv": [5, 13, 7, 14], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 2, 5],
+ "to": [15, 3, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [10, 13, 11, 14], "texture": "#7"},
+ "east": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "south": {"uv": [9, 13, 10, 14], "texture": "#7"},
+ "west": {"uv": [9, 13, 11, 14], "texture": "#7"},
+ "up": {"uv": [9, 13, 11, 14], "rotation": 90, "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 3, 9],
+ "to": [15, 4, 10],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "east": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "south": {"uv": [6, 12, 7, 13], "texture": "#7"},
+ "west": {"uv": [6, 12, 7, 13], "texture": "#7"}
+ }
+ },
+ {
+ "from": [14, 3, 6],
+ "to": [15, 4, 7],
+ "rotation": {"angle": 0, "axis": "y", "origin": [15, 2, 2]},
+ "faces": {
+ "north": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "east": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "south": {"uv": [9, 12, 10, 13], "texture": "#7"},
+ "west": {"uv": [9, 12, 10, 13], "texture": "#7"}
+ }
+ },
+ {
+ "name": "Axis",
+ "from": [6, 0, 6],
+ "to": [10, 16, 10],
+ "faces": {
+ "north": {"uv": [6, 0, 10, 16], "texture": "#8"},
+ "east": {"uv": [6, 0, 10, 16], "texture": "#8"},
+ "south": {"uv": [6, 0, 10, 16], "texture": "#8"},
+ "west": {"uv": [6, 0, 10, 16], "texture": "#8"},
+ "up": {"uv": [6, 6, 10, 10], "texture": "#6"},
+ "down": {"uv": [6, 6, 10, 10], "texture": "#6"}
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "sheet bottom",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [0, 1, 2, 3, 4]
+ },
+ {
+ "name": "sheet top",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [5, 6, 7, 8, 9]
+ },
+ {
+ "name": "pillars",
+ "origin": [14, 1, 0],
+ "color": 0,
+ "children": [10, 11, 12, 13]
+ },
+ 14,
+ {
+ "name": "bars north",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]
+ },
+ {
+ "name": "bars west",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]
+ },
+ {
+ "name": "bars east",
+ "origin": [9, 3, 0],
+ "color": 0,
+ "children": [59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
+ },
+ 81
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher.json b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher.json
new file mode 100644
index 0000000..473abe7
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher.json
@@ -0,0 +1,235 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "textures": {
+ "0": "create_mob_spawners:item/soul_catcher"
+ },
+ "elements": [
+ {
+ "name": "right",
+ "from": [4, 5, 4],
+ "to": [5, 8, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [0, 10, 1, 13], "texture": "#0"},
+ "east": {"uv": [0, 13, 8, 10], "texture": "#0"},
+ "south": {"uv": [7, 10, 8, 13], "texture": "#0"},
+ "west": {"uv": [0, 10, 8, 13], "texture": "#0"},
+ "up": {"uv": [1, 8, 0, 0], "texture": "#0"},
+ "down": {"uv": [9, 6, 10, 14], "texture": "#0"}
+ }
+ },
+ {
+ "name": "left",
+ "from": [11, 5, 4],
+ "to": [12, 8, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [0, 10, 1, 13], "texture": "#0"},
+ "east": {"uv": [0, 13, 8, 10], "texture": "#0"},
+ "south": {"uv": [7, 10, 8, 13], "texture": "#0"},
+ "west": {"uv": [0, 10, 8, 13], "texture": "#0"},
+ "up": {"uv": [1, 8, 0, 0], "texture": "#0"},
+ "down": {"uv": [9, 6, 10, 14], "texture": "#0"}
+ }
+ },
+ {
+ "name": "bottom",
+ "from": [4, 8, 5],
+ "to": [12, 9, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 8, 5], "texture": "#0"},
+ "east": {"uv": [1, 8, 7, 9], "texture": "#0"},
+ "south": {"uv": [0, 8, 8, 9], "texture": "#0"},
+ "west": {"uv": [1, 4, 7, 5], "texture": "#0"},
+ "up": {"uv": [16, 13, 8, 7], "texture": "#0"}
+ }
+ },
+ {
+ "name": "front",
+ "from": [5, 5, 4],
+ "to": [11, 9, 5],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [1, 9, 7, 13], "texture": "#0"},
+ "east": {"uv": [7, 9, 8, 13], "texture": "#0"},
+ "south": {"uv": [1, 9, 7, 13], "texture": "#0"},
+ "west": {"uv": [0, 9, 1, 13], "texture": "#0"},
+ "up": {"uv": [7, 10, 1, 9], "texture": "#0"},
+ "down": {"uv": [1, 12, 7, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "back",
+ "from": [5, 5, 11],
+ "to": [11, 9, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "north": {"uv": [1, 9, 7, 13], "texture": "#0"},
+ "east": {"uv": [7, 9, 8, 13], "texture": "#0"},
+ "south": {"uv": [1, 9, 7, 13], "texture": "#0"},
+ "west": {"uv": [0, 9, 1, 13], "texture": "#0"},
+ "up": {"uv": [7, 10, 1, 9], "texture": "#0"},
+ "down": {"uv": [1, 12, 7, 13], "texture": "#0"}
+ }
+ },
+ {
+ "name": "inside",
+ "from": [5, 7, 5],
+ "to": [11, 7, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "down": {"uv": [16, 0, 10, 6], "texture": "#0"}
+ }
+ },
+ {
+ "name": "left",
+ "from": [11, 1, 4],
+ "to": [12, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 1, 8], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [7, 4, 8, 8], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [0, 0, 1, 8], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 8, 0, 0], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "right",
+ "from": [4, 1, 4],
+ "to": [5, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [1, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 1, 8], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [7, 4, 8, 8], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [0, 8, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [0, 0, 1, 8], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 8, 0, 0], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "front",
+ "from": [5, 0, 4],
+ "to": [11, 4, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [1, 4, 7, 0], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 0, 1, 4], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [1, 4, 7, 0], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [7, 0, 8, 4], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [1, 0, 7, 1], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [1, 3, 7, 4], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "front_top_left",
+ "from": [9, 4, 4],
+ "to": [11, 5, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [9, 4, 4]},
+ "faces": {
+ "north": {"uv": [4, 3, 6, 4], "texture": "#0"},
+ "south": {"uv": [2, 3, 4, 4], "texture": "#0"},
+ "west": {"uv": [0, 0, 1, 1], "texture": "#0"},
+ "up": {"uv": [2, 3, 4, 4], "texture": "#0"}
+ }
+ },
+ {
+ "name": "front_top_right",
+ "from": [5, 4, 4],
+ "to": [7, 5, 5],
+ "rotation": {"angle": 0, "axis": "y", "origin": [5, 4, 4]},
+ "faces": {
+ "north": {"uv": [4, 3, 6, 4], "texture": "#0"},
+ "east": {"uv": [0, 0, 1, 1], "texture": "#0"},
+ "south": {"uv": [2, 3, 4, 4], "texture": "#0"},
+ "up": {"uv": [2, 3, 4, 4], "texture": "#0"}
+ }
+ },
+ {
+ "name": "bottom",
+ "from": [4, 0, 5],
+ "to": [12, 1, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [0, 4, 8, 5], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [1, 4, 7, 5], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [0, 8, 8, 9], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [1, 8, 7, 9], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [16, 13, 8, 7], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "back",
+ "from": [5, 0, 11],
+ "to": [11, 5, 12],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 4, 12]},
+ "faces": {
+ "north": {"uv": [1, 4, 7, 9], "rotation": 180, "texture": "#0"},
+ "east": {"uv": [0, 4, 1, 9], "rotation": 180, "texture": "#0"},
+ "south": {"uv": [1, 4, 7, 9], "rotation": 180, "texture": "#0"},
+ "west": {"uv": [7, 4, 8, 9], "rotation": 180, "texture": "#0"},
+ "up": {"uv": [1, 4, 7, 5], "rotation": 180, "texture": "#0"},
+ "down": {"uv": [7, 5, 1, 4], "rotation": 180, "texture": "#0"}
+ }
+ },
+ {
+ "name": "inside",
+ "from": [5, 2, 5],
+ "to": [11, 2, 11],
+ "rotation": {"angle": 0, "axis": "x", "origin": [8, 5, 12]},
+ "faces": {
+ "up": {"uv": [16, 0, 10, 6], "rotation": 180, "texture": "#0"}
+ }
+ }
+ ],
+ "display": {
+ "thirdperson_righthand": {
+ "rotation": [51, 0, 0],
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "thirdperson_lefthand": {
+ "rotation": [51, 0, 0],
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "firstperson_righthand": {
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "firstperson_lefthand": {
+ "translation": [0, 4, 0],
+ "scale": [0.75, 0.75, 0.75]
+ },
+ "ground": {
+ "translation": [0, 2.75, 0],
+ "scale": [0.35, 0.35, 0.35]
+ },
+ "gui": {
+ "rotation": [25, -145, 0],
+ "translation": [0, 2.75, 0],
+ "scale": [0.9, 0.9, 0.9]
+ },
+ "fixed": {
+ "translation": [0, 3.5, 0]
+ }
+ },
+ "groups": [
+ {
+ "name": "upper half",
+ "origin": [7, 3, 10],
+ "color": 0,
+ "children": [0, 1, 2, 3, 4, 5]
+ },
+ {
+ "name": "lower half",
+ "origin": [7, 3, 10],
+ "color": 0,
+ "children": [6, 7, 8, 9, 10, 11, 12, 13]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear.json b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear.json
new file mode 100644
index 0000000..36e4584
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear.json
@@ -0,0 +1,61 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "textures": {
+ "0": "create_mob_spawners:item/soul_catcher"
+ },
+ "elements": [
+ {
+ "from": [6, 4, 3],
+ "to": [10, 5, 4],
+ "rotation": {"angle": 0, "axis": "z", "origin": [8, 4, 4]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "east": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "west": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "up": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "down": {"uv": [6, 14, 14, 16], "texture": "#0"}
+ }
+ },
+ {
+ "from": [5.64645, 3.85355, 3],
+ "to": [9.64645, 4.85355, 4],
+ "rotation": {"angle": -45, "axis": "z", "origin": [8, 4, 4]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "east": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "west": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "up": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "down": {"uv": [6, 14, 14, 16], "texture": "#0"}
+ }
+ },
+ {
+ "from": [7.5, 2.5, 3],
+ "to": [8.5, 6.5, 4],
+ "rotation": {"angle": 0, "axis": "z", "origin": [8, 4, 4]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "rotation": 270, "texture": "#0"},
+ "east": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "west": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"},
+ "down": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"}
+ }
+ },
+ {
+ "from": [7.14645, 2.35355, 3],
+ "to": [8.14645, 6.35355, 4],
+ "rotation": {"angle": -45, "axis": "z", "origin": [8, 4, 4]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "rotation": 270, "texture": "#0"},
+ "east": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "west": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"},
+ "down": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"}
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear_empty.json b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear_empty.json
new file mode 100644
index 0000000..a5b1839
--- /dev/null
+++ b/src/main/resources/assets/create_mob_spawners/models/item/soul_catcher/gear_empty.json
@@ -0,0 +1,61 @@
+{
+ "credit": "Made with Blockbench",
+ "parent": "block/block",
+ "textures": {
+ "0": "create_mob_spawners:item/soul_catcher"
+ },
+ "elements": [
+ {
+ "from": [7.5, 8.65147, 5.29853],
+ "to": [8.5, 12.65147, 6.29853],
+ "rotation": {"angle": 0, "axis": "y", "origin": [8, 10.65147, 6.34853]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "rotation": 270, "texture": "#0"},
+ "east": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "west": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"},
+ "down": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"}
+ }
+ },
+ {
+ "from": [7.5, 8.65147, 5.29853],
+ "to": [8.5, 12.65147, 6.29853],
+ "rotation": {"angle": -45, "axis": "z", "origin": [8, 10.65147, 6.34853]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "rotation": 270, "texture": "#0"},
+ "east": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "west": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"},
+ "down": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"}
+ }
+ },
+ {
+ "from": [7.5, 8.65147, 5.29853],
+ "to": [8.5, 12.65147, 6.29853],
+ "rotation": {"angle": 45, "axis": "z", "origin": [8, 10.65147, 6.34853]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "rotation": 270, "texture": "#0"},
+ "east": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "west": {"uv": [6, 14, 14, 16], "rotation": 90, "texture": "#0"},
+ "up": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"},
+ "down": {"uv": [14, 14, 16, 16], "rotation": 90, "texture": "#0"}
+ }
+ },
+ {
+ "from": [6, 10.15147, 5.29853],
+ "to": [10, 11.15147, 6.29853],
+ "rotation": {"angle": 0, "axis": "y", "origin": [8, 10.65147, 6.34853]},
+ "faces": {
+ "north": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "east": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "south": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "west": {"uv": [14, 14, 16, 16], "texture": "#0"},
+ "up": {"uv": [6, 14, 14, 16], "texture": "#0"},
+ "down": {"uv": [6, 14, 14, 16], "texture": "#0"}
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner.png b/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner.png
new file mode 100644
index 0000000..60cd94b
Binary files /dev/null and b/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner.png differ
diff --git a/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner_vertical_inside.png b/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner_vertical_inside.png
new file mode 100644
index 0000000..88b50b1
Binary files /dev/null and b/src/main/resources/assets/create_mob_spawners/textures/block/mechanical_spawner_vertical_inside.png differ
diff --git a/src/main/resources/assets/create_mob_spawners/textures/item/soul_catcher.png b/src/main/resources/assets/create_mob_spawners/textures/item/soul_catcher.png
new file mode 100644
index 0000000..8316518
Binary files /dev/null and b/src/main/resources/assets/create_mob_spawners/textures/item/soul_catcher.png differ
diff --git a/src/main/resources/data/create_mob_spawners/loot_tables/blocks/mechanical_spawner.json b/src/main/resources/data/create_mob_spawners/loot_tables/blocks/mechanical_spawner.json
new file mode 100644
index 0000000..3fff503
--- /dev/null
+++ b/src/main/resources/data/create_mob_spawners/loot_tables/blocks/mechanical_spawner.json
@@ -0,0 +1,14 @@
+{
+ "type": "minecraft:block",
+ "pools": [
+ {
+ "entries": [
+ {
+ "type": "minecraft:item",
+ "name": "create_mob_spawners:mechanical_spawner"
+ }
+ ],
+ "rolls": 1.0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/data/create_mob_spawners/recipes/empty_soul_catcher.json b/src/main/resources/data/create_mob_spawners/recipes/empty_soul_catcher.json
new file mode 100644
index 0000000..6b16e8d
--- /dev/null
+++ b/src/main/resources/data/create_mob_spawners/recipes/empty_soul_catcher.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "key": {
+ "A": {
+ "item": "create:brass_sheet"
+ },
+ "B": {
+ "item": "minecraft:shulker_shell"
+ },
+ "C": {
+ "item": "create:brass_casing"
+ },
+ "D": {
+ "item": "minecraft:nether_star"
+ },
+ "E": {
+ "item": "create:cogwheel"
+ }
+ },
+ "pattern": [
+ "ABA",
+ "CDE",
+ "ABA"
+ ],
+ "result": {
+ "item": "create_mob_spawners:empty_soul_catcher"
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/create_mob_spawners/recipes/mechanical_crafting/mechanical_spawner.json b/src/main/resources/data/create_mob_spawners/recipes/mechanical_crafting/mechanical_spawner.json
new file mode 100644
index 0000000..b03eac7
--- /dev/null
+++ b/src/main/resources/data/create_mob_spawners/recipes/mechanical_crafting/mechanical_spawner.json
@@ -0,0 +1,40 @@
+{
+ "type": "create:mechanical_crafting",
+ "acceptMirrored": false,
+ "key": {
+ "A": {
+ "item": "create:brass_casing"
+ },
+ "B": {
+ "item": "create:brass_sheet"
+ },
+ "C": {
+ "item": "create:shaft"
+ },
+ "D": {
+ "item": "minecraft:chain"
+ },
+ "E": {
+ "item": "minecraft:netherite_ingot"
+ },
+ "F": {
+ "item": "minecraft:end_crystal"
+ },
+ "G": {
+ "item": "minecraft:totem_of_undying"
+ },
+ "H": {
+ "item": "create:experience_block"
+ }
+ },
+ "pattern": [
+ "ABCBA",
+ "DEFED",
+ "DEGED",
+ "DEHED",
+ "ABCBA"
+ ],
+ "result": {
+ "item": "create_mob_spawners:mechanical_spawner"
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json b/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json
new file mode 100644
index 0000000..d1a677b
--- /dev/null
+++ b/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json
@@ -0,0 +1,5 @@
+{
+ "values": [
+ "create_mob_spawners:mechanical_spawner"
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png
new file mode 100644
index 0000000..a77fdcd
Binary files /dev/null and b/src/main/resources/icon.png differ
diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta
new file mode 100644
index 0000000..eca79ae
--- /dev/null
+++ b/src/main/resources/pack.mcmeta
@@ -0,0 +1,8 @@
+{
+ "pack": {
+ "description": {
+ "text": "${mod_id} resources"
+ },
+ "pack_format": 15
+ }
+}
\ No newline at end of file