From 3402f31a718ee0c72288cc2cc1480e32488dcd1a Mon Sep 17 00:00:00 2001 From: ybw0014 Date: Thu, 9 Oct 2025 13:03:38 -0400 Subject: [PATCH 1/6] feat: add i18n plugin, relocate existing files for new folder structure --- docs/{ => en}/contributing/contributing-to-docs.md | 0 docs/{ => en}/contributing/getting-started.md | 0 docs/{ => en}/contributing/master-project.md | 0 .../custom-items/adding-a-custom-item.md | 0 .../creating-addons/custom-items/advanced-lore.md | 0 .../creating-addons/custom-items/full-code.md | 0 .../creating-addons/custom-items/img/attribute.png | Bin .../custom-items/img/baguette_of_wisdom_error.png | Bin .../custom-items/img/baguette_of_wisdom_success.png | Bin .../img/boring-baguette-flamethrower.png | Bin .../creating-addons/custom-items/img/formatting.png | Bin .../creating-addons/custom-items/img/gradient.png | Bin .../creating-addons/custom-items/img/hex-color.png | Bin .../custom-items/img/instruction.png | Bin .../custom-items/img/placeholder.png | Bin .../custom-items/img/simple-color.png | Bin .../creating-addons/custom-items/img/unit.png | Bin .../custom-items/making-items-configurable.md | 0 .../creating-addons/custom-items/persistent-data.md | 0 .../creating-addons/custom-items/practice-tasks.md | 0 docs/{ => en}/creating-addons/getting-started.md | 0 .../img/importing-addon-template.png | Bin docs/{ => en}/creating-addons/item_ux.md | 0 .../your-first-item/adding-an-item.md | 0 .../your-first-item/adding-name-and-lore.md | 0 .../creating-addons/your-first-item/full-code.md | 0 .../img/baguette-missing-translation-key.png | Bin .../your-first-item/img/baguette.png | Bin .../your-first-item/img/running-test-server.png | Bin .../your-first-item/practice-tasks.md | 0 .../your-first-item/running-your-addon.md | 0 docs/{ => en}/index.md | 0 .../installation/commands-and-permissions.md | 0 docs/{ => en}/installation/installing-pylon.md | 0 docs/{ => en}/installation/list-of-addons.md | 0 docs/{ => en}/javadocs.md | 0 docs/{ => en}/reference/fluids.md | 0 mkdocs.yml | 10 ++++++++++ requirements.txt | 1 + 39 files changed, 11 insertions(+) rename docs/{ => en}/contributing/contributing-to-docs.md (100%) rename docs/{ => en}/contributing/getting-started.md (100%) rename docs/{ => en}/contributing/master-project.md (100%) rename docs/{ => en}/creating-addons/custom-items/adding-a-custom-item.md (100%) rename docs/{ => en}/creating-addons/custom-items/advanced-lore.md (100%) rename docs/{ => en}/creating-addons/custom-items/full-code.md (100%) rename docs/{ => en}/creating-addons/custom-items/img/attribute.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/baguette_of_wisdom_error.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/baguette_of_wisdom_success.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/boring-baguette-flamethrower.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/formatting.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/gradient.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/hex-color.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/instruction.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/placeholder.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/simple-color.png (100%) rename docs/{ => en}/creating-addons/custom-items/img/unit.png (100%) rename docs/{ => en}/creating-addons/custom-items/making-items-configurable.md (100%) rename docs/{ => en}/creating-addons/custom-items/persistent-data.md (100%) rename docs/{ => en}/creating-addons/custom-items/practice-tasks.md (100%) rename docs/{ => en}/creating-addons/getting-started.md (100%) rename docs/{ => en}/creating-addons/img/importing-addon-template.png (100%) rename docs/{ => en}/creating-addons/item_ux.md (100%) rename docs/{ => en}/creating-addons/your-first-item/adding-an-item.md (100%) rename docs/{ => en}/creating-addons/your-first-item/adding-name-and-lore.md (100%) rename docs/{ => en}/creating-addons/your-first-item/full-code.md (100%) rename docs/{ => en}/creating-addons/your-first-item/img/baguette-missing-translation-key.png (100%) rename docs/{ => en}/creating-addons/your-first-item/img/baguette.png (100%) rename docs/{ => en}/creating-addons/your-first-item/img/running-test-server.png (100%) rename docs/{ => en}/creating-addons/your-first-item/practice-tasks.md (100%) rename docs/{ => en}/creating-addons/your-first-item/running-your-addon.md (100%) rename docs/{ => en}/index.md (100%) rename docs/{ => en}/installation/commands-and-permissions.md (100%) rename docs/{ => en}/installation/installing-pylon.md (100%) rename docs/{ => en}/installation/list-of-addons.md (100%) rename docs/{ => en}/javadocs.md (100%) rename docs/{ => en}/reference/fluids.md (100%) diff --git a/docs/contributing/contributing-to-docs.md b/docs/en/contributing/contributing-to-docs.md similarity index 100% rename from docs/contributing/contributing-to-docs.md rename to docs/en/contributing/contributing-to-docs.md diff --git a/docs/contributing/getting-started.md b/docs/en/contributing/getting-started.md similarity index 100% rename from docs/contributing/getting-started.md rename to docs/en/contributing/getting-started.md diff --git a/docs/contributing/master-project.md b/docs/en/contributing/master-project.md similarity index 100% rename from docs/contributing/master-project.md rename to docs/en/contributing/master-project.md diff --git a/docs/creating-addons/custom-items/adding-a-custom-item.md b/docs/en/creating-addons/custom-items/adding-a-custom-item.md similarity index 100% rename from docs/creating-addons/custom-items/adding-a-custom-item.md rename to docs/en/creating-addons/custom-items/adding-a-custom-item.md diff --git a/docs/creating-addons/custom-items/advanced-lore.md b/docs/en/creating-addons/custom-items/advanced-lore.md similarity index 100% rename from docs/creating-addons/custom-items/advanced-lore.md rename to docs/en/creating-addons/custom-items/advanced-lore.md diff --git a/docs/creating-addons/custom-items/full-code.md b/docs/en/creating-addons/custom-items/full-code.md similarity index 100% rename from docs/creating-addons/custom-items/full-code.md rename to docs/en/creating-addons/custom-items/full-code.md diff --git a/docs/creating-addons/custom-items/img/attribute.png b/docs/en/creating-addons/custom-items/img/attribute.png similarity index 100% rename from docs/creating-addons/custom-items/img/attribute.png rename to docs/en/creating-addons/custom-items/img/attribute.png diff --git a/docs/creating-addons/custom-items/img/baguette_of_wisdom_error.png b/docs/en/creating-addons/custom-items/img/baguette_of_wisdom_error.png similarity index 100% rename from docs/creating-addons/custom-items/img/baguette_of_wisdom_error.png rename to docs/en/creating-addons/custom-items/img/baguette_of_wisdom_error.png diff --git a/docs/creating-addons/custom-items/img/baguette_of_wisdom_success.png b/docs/en/creating-addons/custom-items/img/baguette_of_wisdom_success.png similarity index 100% rename from docs/creating-addons/custom-items/img/baguette_of_wisdom_success.png rename to docs/en/creating-addons/custom-items/img/baguette_of_wisdom_success.png diff --git a/docs/creating-addons/custom-items/img/boring-baguette-flamethrower.png b/docs/en/creating-addons/custom-items/img/boring-baguette-flamethrower.png similarity index 100% rename from docs/creating-addons/custom-items/img/boring-baguette-flamethrower.png rename to docs/en/creating-addons/custom-items/img/boring-baguette-flamethrower.png diff --git a/docs/creating-addons/custom-items/img/formatting.png b/docs/en/creating-addons/custom-items/img/formatting.png similarity index 100% rename from docs/creating-addons/custom-items/img/formatting.png rename to docs/en/creating-addons/custom-items/img/formatting.png diff --git a/docs/creating-addons/custom-items/img/gradient.png b/docs/en/creating-addons/custom-items/img/gradient.png similarity index 100% rename from docs/creating-addons/custom-items/img/gradient.png rename to docs/en/creating-addons/custom-items/img/gradient.png diff --git a/docs/creating-addons/custom-items/img/hex-color.png b/docs/en/creating-addons/custom-items/img/hex-color.png similarity index 100% rename from docs/creating-addons/custom-items/img/hex-color.png rename to docs/en/creating-addons/custom-items/img/hex-color.png diff --git a/docs/creating-addons/custom-items/img/instruction.png b/docs/en/creating-addons/custom-items/img/instruction.png similarity index 100% rename from docs/creating-addons/custom-items/img/instruction.png rename to docs/en/creating-addons/custom-items/img/instruction.png diff --git a/docs/creating-addons/custom-items/img/placeholder.png b/docs/en/creating-addons/custom-items/img/placeholder.png similarity index 100% rename from docs/creating-addons/custom-items/img/placeholder.png rename to docs/en/creating-addons/custom-items/img/placeholder.png diff --git a/docs/creating-addons/custom-items/img/simple-color.png b/docs/en/creating-addons/custom-items/img/simple-color.png similarity index 100% rename from docs/creating-addons/custom-items/img/simple-color.png rename to docs/en/creating-addons/custom-items/img/simple-color.png diff --git a/docs/creating-addons/custom-items/img/unit.png b/docs/en/creating-addons/custom-items/img/unit.png similarity index 100% rename from docs/creating-addons/custom-items/img/unit.png rename to docs/en/creating-addons/custom-items/img/unit.png diff --git a/docs/creating-addons/custom-items/making-items-configurable.md b/docs/en/creating-addons/custom-items/making-items-configurable.md similarity index 100% rename from docs/creating-addons/custom-items/making-items-configurable.md rename to docs/en/creating-addons/custom-items/making-items-configurable.md diff --git a/docs/creating-addons/custom-items/persistent-data.md b/docs/en/creating-addons/custom-items/persistent-data.md similarity index 100% rename from docs/creating-addons/custom-items/persistent-data.md rename to docs/en/creating-addons/custom-items/persistent-data.md diff --git a/docs/creating-addons/custom-items/practice-tasks.md b/docs/en/creating-addons/custom-items/practice-tasks.md similarity index 100% rename from docs/creating-addons/custom-items/practice-tasks.md rename to docs/en/creating-addons/custom-items/practice-tasks.md diff --git a/docs/creating-addons/getting-started.md b/docs/en/creating-addons/getting-started.md similarity index 100% rename from docs/creating-addons/getting-started.md rename to docs/en/creating-addons/getting-started.md diff --git a/docs/creating-addons/img/importing-addon-template.png b/docs/en/creating-addons/img/importing-addon-template.png similarity index 100% rename from docs/creating-addons/img/importing-addon-template.png rename to docs/en/creating-addons/img/importing-addon-template.png diff --git a/docs/creating-addons/item_ux.md b/docs/en/creating-addons/item_ux.md similarity index 100% rename from docs/creating-addons/item_ux.md rename to docs/en/creating-addons/item_ux.md diff --git a/docs/creating-addons/your-first-item/adding-an-item.md b/docs/en/creating-addons/your-first-item/adding-an-item.md similarity index 100% rename from docs/creating-addons/your-first-item/adding-an-item.md rename to docs/en/creating-addons/your-first-item/adding-an-item.md diff --git a/docs/creating-addons/your-first-item/adding-name-and-lore.md b/docs/en/creating-addons/your-first-item/adding-name-and-lore.md similarity index 100% rename from docs/creating-addons/your-first-item/adding-name-and-lore.md rename to docs/en/creating-addons/your-first-item/adding-name-and-lore.md diff --git a/docs/creating-addons/your-first-item/full-code.md b/docs/en/creating-addons/your-first-item/full-code.md similarity index 100% rename from docs/creating-addons/your-first-item/full-code.md rename to docs/en/creating-addons/your-first-item/full-code.md diff --git a/docs/creating-addons/your-first-item/img/baguette-missing-translation-key.png b/docs/en/creating-addons/your-first-item/img/baguette-missing-translation-key.png similarity index 100% rename from docs/creating-addons/your-first-item/img/baguette-missing-translation-key.png rename to docs/en/creating-addons/your-first-item/img/baguette-missing-translation-key.png diff --git a/docs/creating-addons/your-first-item/img/baguette.png b/docs/en/creating-addons/your-first-item/img/baguette.png similarity index 100% rename from docs/creating-addons/your-first-item/img/baguette.png rename to docs/en/creating-addons/your-first-item/img/baguette.png diff --git a/docs/creating-addons/your-first-item/img/running-test-server.png b/docs/en/creating-addons/your-first-item/img/running-test-server.png similarity index 100% rename from docs/creating-addons/your-first-item/img/running-test-server.png rename to docs/en/creating-addons/your-first-item/img/running-test-server.png diff --git a/docs/creating-addons/your-first-item/practice-tasks.md b/docs/en/creating-addons/your-first-item/practice-tasks.md similarity index 100% rename from docs/creating-addons/your-first-item/practice-tasks.md rename to docs/en/creating-addons/your-first-item/practice-tasks.md diff --git a/docs/creating-addons/your-first-item/running-your-addon.md b/docs/en/creating-addons/your-first-item/running-your-addon.md similarity index 100% rename from docs/creating-addons/your-first-item/running-your-addon.md rename to docs/en/creating-addons/your-first-item/running-your-addon.md diff --git a/docs/index.md b/docs/en/index.md similarity index 100% rename from docs/index.md rename to docs/en/index.md diff --git a/docs/installation/commands-and-permissions.md b/docs/en/installation/commands-and-permissions.md similarity index 100% rename from docs/installation/commands-and-permissions.md rename to docs/en/installation/commands-and-permissions.md diff --git a/docs/installation/installing-pylon.md b/docs/en/installation/installing-pylon.md similarity index 100% rename from docs/installation/installing-pylon.md rename to docs/en/installation/installing-pylon.md diff --git a/docs/installation/list-of-addons.md b/docs/en/installation/list-of-addons.md similarity index 100% rename from docs/installation/list-of-addons.md rename to docs/en/installation/list-of-addons.md diff --git a/docs/javadocs.md b/docs/en/javadocs.md similarity index 100% rename from docs/javadocs.md rename to docs/en/javadocs.md diff --git a/docs/reference/fluids.md b/docs/en/reference/fluids.md similarity index 100% rename from docs/reference/fluids.md rename to docs/en/reference/fluids.md diff --git a/mkdocs.yml b/mkdocs.yml index cb38265..78e650f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -76,3 +76,13 @@ markdown_extensions: plugins: - open-in-new-tab + - i18n: + docs_structure: folder + fallback_to_default: true + reconfigure_material: true + reconfigure_search: true + languages: + - locale: en + default: true + name: English + build: true diff --git a/requirements.txt b/requirements.txt index 0123413..cee5e9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ mkdocs mkdocs-material mkdocs-open-in-new-tab +mkdocs-static-i18n[material] From 78a2f4ca51999bdd56f4d5858c8dc5ab1dbb97be Mon Sep 17 00:00:00 2001 From: ybw0014 Date: Sat, 11 Oct 2025 21:24:29 -0400 Subject: [PATCH 2/6] fix(ci): attempt to fix CI --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d91d0af..be918e9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: run: mkdocs build - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@4b09552702d0b65573696410d4707c765da2630b + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e with: personal_token: ${{ secrets.DOCS_DEPLOY_TOKEN }} external_repository: pylonmc/pylonmc.github.io From a3eec0f522638fd4e491b77caea8f528c16b553a Mon Sep 17 00:00:00 2001 From: Idra Date: Tue, 14 Oct 2025 17:09:09 +0100 Subject: [PATCH 3/6] Create LICENSE --- LICENSE | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER 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. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. From 93301976a658fafb11d5eb086163acfa6d1d0fe6 Mon Sep 17 00:00:00 2001 From: ybw0014 Date: Wed, 15 Oct 2025 15:46:13 -0400 Subject: [PATCH 4/6] chore: reorganize images --- docs/{ => en}/img/hovering-over-research-pack.png | Bin docs/{ => en}/img/looking-at-copper-fluid-tank.png | Bin docs/{ => en}/img/looking-at-research.png | Bin docs/{ => en}/img/medkit.png | Bin docs/{ => en}/img/placing-pipes.png | Bin docs/{ => en}/img/searching-items.png | Bin docs/en/index.md | 9 +++++---- 7 files changed, 5 insertions(+), 4 deletions(-) rename docs/{ => en}/img/hovering-over-research-pack.png (100%) rename docs/{ => en}/img/looking-at-copper-fluid-tank.png (100%) rename docs/{ => en}/img/looking-at-research.png (100%) rename docs/{ => en}/img/medkit.png (100%) rename docs/{ => en}/img/placing-pipes.png (100%) rename docs/{ => en}/img/searching-items.png (100%) diff --git a/docs/img/hovering-over-research-pack.png b/docs/en/img/hovering-over-research-pack.png similarity index 100% rename from docs/img/hovering-over-research-pack.png rename to docs/en/img/hovering-over-research-pack.png diff --git a/docs/img/looking-at-copper-fluid-tank.png b/docs/en/img/looking-at-copper-fluid-tank.png similarity index 100% rename from docs/img/looking-at-copper-fluid-tank.png rename to docs/en/img/looking-at-copper-fluid-tank.png diff --git a/docs/img/looking-at-research.png b/docs/en/img/looking-at-research.png similarity index 100% rename from docs/img/looking-at-research.png rename to docs/en/img/looking-at-research.png diff --git a/docs/img/medkit.png b/docs/en/img/medkit.png similarity index 100% rename from docs/img/medkit.png rename to docs/en/img/medkit.png diff --git a/docs/img/placing-pipes.png b/docs/en/img/placing-pipes.png similarity index 100% rename from docs/img/placing-pipes.png rename to docs/en/img/placing-pipes.png diff --git a/docs/img/searching-items.png b/docs/en/img/searching-items.png similarity index 100% rename from docs/img/searching-items.png rename to docs/en/img/searching-items.png diff --git a/docs/en/index.md b/docs/en/index.md index ad5827f..e29bbbd 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -3,7 +3,7 @@ Pylon is an **upcoming** Minecraft Java plugin that will hugely expand vanilla g Pylon uses an addon system, meaning anyone can add content to Pylon by writing an addon for it! It also comes with a number of really useful features, such as: - First-class translation support, meaning each player can select their own language. -- Extensive configuration options, including per-machine configuration. +- Extensive configuration options, including per-machine configuration. - An intuitive and user-friendly guide to help players figure out the plugin. ## :frame_photo: Pylon in pictures (so far) @@ -25,9 +25,9 @@ Pylon uses an addon system, meaning anyone can add content to Pylon by writing a We have built Pylon with performance and stability in mind from day 1: -- Pylon makes **extensive** use of caching and will run most performance-intensive systems asynchronously, as well as makeing use of modern concurrency/performance features such as coroutines. -- In addition, the plugin will have a broad range of config options to help performance - including per-machine tick rates, the ability to limit the number of machines placed by player/chunk/etc, the tick rate of fluid/energy/etc, and more. -- The way Pylon is designed minimises the chance of data corruption and has a range of error-handling mechanisms if something does go wrong. +- Pylon makes **extensive** use of caching and will run most performance-intensive systems asynchronously, as well as makeing use of modern concurrency/performance features such as coroutines. +- In addition, the plugin will have a broad range of config options to help performance - including per-machine tick rates, the ability to limit the number of machines placed by player/chunk/etc, the tick rate of fluid/energy/etc, and more. +- The way Pylon is designed minimises the chance of data corruption and has a range of error-handling mechanisms if something does go wrong. Note: Pylon will likely not be fully compatible with bedrock. ## :calendar: Provisional timeline @@ -75,4 +75,5 @@ We also have help from: If you're interested in helping out, drop us a message on our Discord server! You don't have to be an expert to help, a little plugin development experience is enough - there are plenty of tasks to do, ranging from trivial to exceptionally complex, so just let us know if you're interested and we can find something for you to do. ## :question: Got questions? + Drop a message on our Discord server and we'll be happy to answer. From 4f4fd44707ad3bec7d3e0ed743694ba7af1dd645 Mon Sep 17 00:00:00 2001 From: LordIdra Date: Mon, 27 Oct 2025 22:18:14 +0000 Subject: [PATCH 5/6] Update permission/command docs --- .../installation/commands-and-permissions.md | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/en/installation/commands-and-permissions.md b/docs/en/installation/commands-and-permissions.md index 9a13502..082f71e 100644 --- a/docs/en/installation/commands-and-permissions.md +++ b/docs/en/installation/commands-and-permissions.md @@ -1,27 +1,30 @@ # Commands & permissions ## Default commands -| Command | Permission | -|------------------------------------|------------------------------------------| -| `/py` | `pylon.command.guide` | -| `/py guide` | `pylon.command.guide` | -| `/py research discover ` | `pylon.command.research.discover` | -| `/py research list` | `pylon.command.research.list` | -| `/py research points me` | `pylon.command.research.points.get.self` | -| `/py waila` | `pylon.command.waila` | +| Command | Permission | Description | +|------------------------------------|------------------------------------------|-------------| +| `/py` | `pylon.command.guide` | Opens the Pylon guide +| `/py guide` | `pylon.command.guide` | Gives you the Pylon guide ## Admin commands -| Command | Permission | -|--------------------------------------------------|-------------------------------------| -| `/py debug` | `pylon.command.debug` | -| `/py give [amount]` | `pylon.command.give` | -| `/py key` | `pylon.command.key` | -| `/py research points get ` | `pylon.command.research.points.get` | -| `/py research add ` | `pylon.command.research.modify` | -| `/py research addall ` | `pylon.command.research.modify` | -| `/py research remove ` | `pylon.command.research.modify` | -| `/py research points add ` | `pylon.command.research.points.set` | -| `/py research points set ` | `pylon.command.research.points.set` | -| `/py research points subtract ` | `pylon.command.research.points.set` | -| `/py setblock ` | `pylon.command.setblock` | -| `/py confetti ` | `pylon.command.confetti` | +| Command | Permission | Description | +|--------------------------------------------------|------------------------------------------|-------------| +| `/py confetti [speed] [lifetime]` | `pylon.command.confetti` | Spawns confetti particles +| `/py debug` | `pylon.command.debug` | Gives you the debug item, which can be used to view Pylon block/entity data and forcibly remove Pylon blocks/entities +| `/py exposerecipeconfig ` | `pylon.command.exposerecipeconfig` | Allows recipes to be edited for a specific recipe stype. Doing this will prevent any recipes of the given type from being automatically updated, so use with caution! +| `/py give [amount]` | `pylon.command.give` | Gives a player a Pylon item +| `/py key` | `pylon.command.key` | Shows you the key of the Pylon item you're holding in your hand +| `/py research add ` | `pylon.command.research.add` | Adds the given research to a player +| `/py research addall ` | `pylon.command.research.addall` | Adds all researches to a player +| `/py research remove ` | `pylon.command.research.remove` | Removes the given research from a player +| `/py research points add ` | `pylon.command.research.points.add` | Adds research points to a player +| `/py research points get ` | `pylon.command.research.points.get` | Shows you the number of research points a player has +| `/py research points set ` | `pylon.command.research.points.set` | Sets the number of research points a player has +| `/py research points subtract ` | `pylon.command.research.points.subtract` | Removes research points from a player +| `/py setblock ` | `pylon.command.setblock` | Sets the given Pylon block at the coordinates provided + +## Other permissions +| Permission | Description | +|--------------------------------|-------------| +| `pylon.guide.cheat` | Allows you to use drop, ctrl+drop, or middle click to cheat in items from the Pylon guide | +| `pylon.guide.view_admin_pages` | Allows you to see admin-only guide pages | From d31d609f5969ad48202cdafe031c009ddb6b37bf Mon Sep 17 00:00:00 2001 From: LordIdra Date: Sun, 2 Nov 2025 20:58:05 +0000 Subject: [PATCH 6/6] many various improvements --- docs/en/contributing/getting-started.md | 2 +- .../custom-items/a-complete-example.md | 734 ++++++++++++++++++ .../custom-items/adding-a-custom-item.md | 14 +- .../custom-items/advanced-lore.md | 26 +- .../creating-addons/custom-items/full-code.md | 22 +- .../custom-items/making-items-configurable.md | 49 +- .../custom-items/persistent-data.md | 732 ++--------------- .../the-basics/adding-a-recipe.md | 44 ++ .../the-basics/adding-a-research.md | 54 ++ .../adding-an-item.md | 2 +- .../adding-name-and-lore.md | 8 +- .../full-code.md | 6 +- .../img/baguette-missing-translation-key.png | Bin .../img/baguette.png | Bin .../img/running-test-server.png | Bin .../practice-tasks.md | 0 .../running-your-addon.md | 4 +- mkdocs.yml | 15 +- 18 files changed, 965 insertions(+), 747 deletions(-) create mode 100644 docs/en/creating-addons/custom-items/a-complete-example.md create mode 100644 docs/en/creating-addons/the-basics/adding-a-recipe.md create mode 100644 docs/en/creating-addons/the-basics/adding-a-research.md rename docs/en/creating-addons/{your-first-item => the-basics}/adding-an-item.md (94%) rename docs/en/creating-addons/{your-first-item => the-basics}/adding-name-and-lore.md (88%) rename docs/en/creating-addons/{your-first-item => the-basics}/full-code.md (87%) rename docs/en/creating-addons/{your-first-item => the-basics}/img/baguette-missing-translation-key.png (100%) rename docs/en/creating-addons/{your-first-item => the-basics}/img/baguette.png (100%) rename docs/en/creating-addons/{your-first-item => the-basics}/img/running-test-server.png (100%) rename docs/en/creating-addons/{your-first-item => the-basics}/practice-tasks.md (100%) rename docs/en/creating-addons/{your-first-item => the-basics}/running-your-addon.md (89%) diff --git a/docs/en/contributing/getting-started.md b/docs/en/contributing/getting-started.md index 72cf860..2541cab 100644 --- a/docs/en/contributing/getting-started.md +++ b/docs/en/contributing/getting-started.md @@ -10,7 +10,7 @@ Pylon Base is written in Java. 2. If you're using IntelliJ, it'll set everything up automatically. If not, run `./gradlew`. This will clone the `pylon-core` and `pylon-base` repositories. 3. If you want to submit your changes to the Pylon project, **delete the `pylon-core` or `pylon-base` directory (depending on which one you want to contribute to), fork the pylon-core or pylon-base repository, and clone your fork into the same directory.** Otherwise, you won't be able to open a pull request with your changes (unless you're a Pylon developer and have access to the Pylon repositories). -See the [Pylon Master Project](./master-project) page for more information about the master repository. +See the [Pylon Master Project](master-project.md) page for more information about the master repository. ## Submitting your contributions diff --git a/docs/en/creating-addons/custom-items/a-complete-example.md b/docs/en/creating-addons/custom-items/a-complete-example.md new file mode 100644 index 0000000..a7ef405 --- /dev/null +++ b/docs/en/creating-addons/custom-items/a-complete-example.md @@ -0,0 +1,734 @@ +# A complete example + +## Overview + +In this section, we'll create an item that incorporates all the concepts discussed in this section so far. + +--- + +## The Baguette of Wisdom + +### The idea + +[Studies have shown](https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ) that those who eat baguettes have a 68% higher IQ on average. Let's use this as inspiration for our addon. We'll create a 'Baguette of Wisdom' which allows you to transfer experience from one player to another. + +Here's the plan: + +- The baguette has a maximum XP capacity +- You can right click with the baguette to 'charge' it with experience +- You can shift right click with the baguette to discharge the experience + +To do this, we're going to need to keep track of how much experience the baguette has inside of it. + +### Creating the item + +You know the drill by now: + +=== "Java" + ```java title="MyAddon.java" + NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .build(); + PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); + BasePages.FOOD.addItem(baguetteOfWisdomKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" + val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .build() + PylonItem.register(baguetteOfWisdom) + BasePages.FOOD.addItem(baguetteOfWisdomKey) + ``` + + + +=== "Java" + ```java title="BaguetteOfWisdom.java" + public class BaguetteOfWisdom extends PylonItem { + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) + ``` + +```yaml title="en.yml" +item: + baguette_of_wisdom: + name: "Baguette of Wisdom" + lore: |- + Use the power of baguettes to transfer XP +``` + +### Adding a config for the XP capacity + +Now, let's add a config value for the max XP capacity: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="2" + public class BaguetteOfWisdom extends PylonItem { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", ConfigAdapter.INT); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", ConfigAdapter.INT) + } + ``` + +```yaml title="baguette_of_wisdom.yml" +xp-capacity: 200 +``` + +We'll be using this later. + +### Improving the lore + +Let's add some instructions to the lore, and attributes for the max charge and current charge: + +```yaml title="en.yml" hl_lines="6-9" +item: + baguette_of_wisdom: + name: "Baguette of Wisdom" + lore: |- + Use the power of baguettes to transfer XP + Right click to charge with XP + Shift right click to discharge stored XP + XP capacity: %xp_capacity% + Stored XP: %stored_xp% +``` + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="8-14" + public class BaguetteOfWisdom extends PylonItem { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", ConfigAdapter.INT); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-7" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", ConfigAdapter.class) + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ) + } + ``` + +All of this should be familiar from the [advanced lore] section. + +### The charge/discharge mechanic + +Next, let's allow the player to charge by right clicking, and discharge by shift right clicking. + +We can use the [PylonInteractor] class to do this: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="1 16-23" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", ConfigAdapter.INT); + + public BaguetteOfWisdom(@NotNull ItemStack stack) { + super(stack); + } + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ); + } + + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + // TODO charge logic + } + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="1 9-15" + + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + private val xpCapacity: Int = settings.getOrThrow("xp-capacity", ConfigAdapter.class) + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) + // TODO add stored_xp placeholder + ) + + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + // TODO charge logic + } + } + } + ``` + +### The charge logic + +Let's now do the **charge** logic. In order to charge a Baguette of Wisdom, we need to store its charge level. As mentioned beforehand, we can use the item's [persistent data container] to do this. To start with, let's just set the charge level to 50: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="6-10" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + 50 + )); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-11" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 50 + ) + } + } + } + ``` + +As you can see, we need to provide three things to set a PDC value: the **key**, the **serializer**, and the **value**. + +The serializer is just a 'thing' that describes how to convert your type into a more primitive type that can be stored on disk - we won't go into details. You need a serializer for every type that you want to store - so you can't store, for example, `MyAddon` in a persistent data container as there is no serializer for it and it doesn't make sense to create one anyway. + +!!! info "You can find a full list of serializers [here](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.html)" + +Ok. But what we really need to do is 'top up' the stored xp using the player's experience: + +1. Read how much XP we already have stored +2. Figure out how much XP we need to take to get to `xpCapacity` +3. Take as much XP from the player as we can to get there +4. Set the new XP amount + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="6-24" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // TODO discharge logic + } else { + // 1. Read how much XP we already have stored + int xp = getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + int extraXpNeeded = xpCapacity - xp; + + // 3. Take as much XP from the player as we can to get there + int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); + event.getPlayer().giveExp(-xpToTake); + + // 4. Set the new stored XP amount + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + xp + xpToTake + )); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-25" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // TODO discharge logic + } else { + // 1. Read how much XP we already have stored + val xp = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + val extraXpNeeded = xpCapacity - xp + + // 3. Take as much XP from the player as we can to get there + val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) + event.player.giveExp(-xpToTake) + + // 4. Set the new stored XP amount + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + xp + xpToTake + ) + } + } + } + ``` + +### The discharge logic + +And now for the discharge logic, which is quite similar: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="4-18" + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // 1. Read how much XP we have stored + int xp = getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + + // 2. Give all the XP to the player + event.getPlayer().giveExp(xp); + + // 3. Set the stored XP to 0 + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + 0 + )); + } else { + ... + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-19" + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // 1. Read how much XP we have stored + val xp = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + + // 2. Give all the XP to the player + event.player.giveExp(xp) + + // 3. Set the stored XP to 0 + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + } else { + ... + } + } + ``` + +### Adding a placeholder + +Finally, let's add in the placeholder for the stored charge: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="5-10" + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( + getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + )) + ) + ); + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-8" + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( + stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + )) + ) + ``` + +### Testing it out + +Now let's start the server and test our glorious new item. Try giving yourself a Baguette of Wisdom. + +Obviously, as I am an expert programmer, it will work perfectly the first time and nothing will go wro- *huh*? + +![Baguette of wisdom error](img/baguette_of_wisdom_error.png) + +What nonsense is this? Have the brits sabotaged us again?! + +When something like this happens, your first port of call should always be the server console to see if any errors have been logged. And indeed, if you have a look in the console you should find the following error: + +```title="console" +[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null +[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.ItemButton.getItemProvider(ItemButton.kt:47) +[12:51:22 WARN]: at xyz.xenondevs.invui.gui.SlotElement$ItemSlotElement.getItemStack(SlotElement.java:44) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.redrawItem(AbstractWindow.java:109) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.initItems(AbstractSingleWindow.java:58) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.open(AbstractWindow.java:279) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow$AbstractBuilder.open(AbstractWindow.java:679) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.pages.base.GuidePage.open(GuidePage.kt:28) +[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.PageButton.handleClick(PageButton.kt:38) +[12:51:22 WARN]: at xyz.xenondevs.invui.gui.AbstractGui.handleClick(AbstractGui.java:95) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.handleClick(AbstractSingleWindow.java:84) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.handleClickEvent(AbstractWindow.java:199) +[12:51:22 WARN]: at xyz.xenondevs.invui.window.WindowManager.handleInventoryClick(WindowManager.java:117) +[12:51:22 WARN]: at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:80) +[12:51:22 WARN]: at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:71) +[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) +[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) +[12:51:22 WARN]: at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:628) +[12:51:22 WARN]: at net.minecraft.server.network.ServerGamePacketListenerImpl.handleContainerClick(ServerGamePacketListenerImpl.java:3208) +[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:59) +[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:14) +[12:51:22 WARN]: at net.minecraft.network.protocol.PacketUtils.lambda$ensureRunningOnSameThread$0(PacketUtils.java:29) +[12:51:22 WARN]: at net.minecraft.server.TickTask.run(TickTask.java:18) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.doRunTask(BlockableEventLoop.java:155) +[12:51:22 WARN]: at net.minecraft.util.thread.ReentrantBlockableEventLoop.doRunTask(ReentrantBlockableEventLoop.java:24) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:1450) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:176) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.pollTask(BlockableEventLoop.java:129) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTaskInternal(MinecraftServer.java:1430) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTask(MinecraftServer.java:1424) +[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.managedBlock(BlockableEventLoop.java:139) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.managedBlock(MinecraftServer.java:1381) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.waitUntilNextTick(MinecraftServer.java:1389) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1266) +[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.lambda$spin$2(MinecraftServer.java:310) +``` + +Wow. That's a fat error. But the lines we are most interested in are right at the top: + +```title="console" +[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null +[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) +``` + +So it looks like the error occurred on line 28, in the `getPlaceholders` function, where we try to read from the persistent data container. Apparently, the 'stored_xp' key couldn't be found in the PDC, because the call to `getStack().getPersistentDataContainer().get(...)` returned null. + +!!! question "Wait, why did `getPlaceholders` get called and error *now*? We haven't given ourselves the item yet." + Simply put, the guide also needs to call `getPlaceholders` to display the item to you. The error only appears once you open the guide - or once you give yourself the item with `/py give`. + +!!! Note "Kotlin - null safety" + If you've been following along in Kotlin, you may have noticed that your error is different from the one above. Additionally, if you played around with the code a bit, you may have noticed that the Kotlin code refuses to compile unless you add a `!!` after the call to `get(...)` in the `getPlaceholders` function. This is because Kotlin actually tracks nulls in the type system, and will error during compile time instead of run time if you try to use a potentially null value without checking for null first. The `!!` tells the compiler, "hey, I know what I'm doing, this value will never be null." This is one of the advantages of using Kotlin over Java, as it can help catch potential null pointer exceptions before they even happen, and is one of the reasons why Pylon Core is written in Kotlin. In this situation, an Elvis operator (`?:`) or a call to `getOrDefault` would have been more appropriate, but for the purposes of this tutorial, we will leave it as is. + +This actually makes perfect sense if you think about it. At no point do we set a default value for the stored XP, so of course any call to get it will return null. + +### Adding a default value + +To add a default value for stored XP to the PDC, we can modify the itemstack itself when we create it: + +=== "Java" + ```java title="MyAddon.java" hl_lines="3-7" + NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc(pdc -> pdc.set( + new NamespacedKey(this, "stored_xp"), + PylonSerializers.INTEGER, + 0 + )) + .build(); + PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); + BasePages.FOOD.addItem(baguetteOfWisdomKey); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" hl_lines="3-9" + val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc { pdc -> + pdc.set( + NamespacedKey(this, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + .build() + PylonItem.register(baguetteOfWisdom) + BasePages.FOOD.addItem(baguetteOfWisdomKey) + ``` + +Now let's try again. + +![Baguette of wisdom success](img/baguette_of_wisdom_success.png) + +Ah, perfect! + +One last thing left to do... + +### Cleaning up + +The Baguette of Wisdom works, but there are some improvements we can make. + +First, we could pull out the get/set code into their own functions: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="4-10 12-17" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + ... + + public void setStoredXp(int xp) { + getStack().editPersistentDataContainer(pdc -> pdc.set( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER, + xp + )); + } + + public int getStoredXp() { + return getStack().getPersistentDataContainer().get( + new NamespacedKey(MyAddon.getInstance(), "stored_xp"), + PylonSerializers.INTEGER + ); + } + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-17" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + ... + + var storedXp: Int + get() = stack.persistentDataContainer.get( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER + )!! + set(value) { + stack.editPersistentDataContainer { pdc -> + pdc.set( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + value + ) + } + } + } + ``` + + !!! Note "Delegate" + Alternatively, Pylon provides a property delegate for persistent data values that can be used to simplify this even further: + + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-8" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + ... + + var storedXp: Int by persistentData( + NamespacedKey(MyAddon.instance, "stored_xp"), + PylonSerializers.INTEGER, + 0 + ) + } + ``` + +And now, we can use these functions in the rest of the code, which is much cleaner: + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="9 17 23 26 36" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + + ... + + @Override + public @NotNull List getPlaceholders() { + return List.of( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(getStoredXp())) + ); + } + + @Override + public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { + if (event.getPlayer().isSneaking()) { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Give all the XP to the player + event.getPlayer().giveExp(xp); + + // 3. Set the stored XP to 0 + setStoredXp(0); + } else { + // 1. Read how much XP we already have stored + int xp = getStoredXp(); + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + int extraXpNeeded = xpCapacity - xp; + + // 3. Take as much XP from the player as we can to get there + int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); + event.getPlayer().giveExp(-xpToTake); + + // 4. Set the new stored XP amount + setStoredXp(xp + xpToTake); + } + } + + ... + } + ``` +=== "Kotlin" + + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="7 13 19 22 32" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + + ... + + override fun getPlaceholders() = listOf( + PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), + PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(storedXp)) + ) + + override fun onUsedToRightClick(event: PlayerInteractEvent) { + if (event.player.isSneaking) { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Give all the XP to the player + event.player.giveExp(xp) + + // 3. Set the stored XP to 0 + storedXp = 0 + } else { + // 1. Read how much XP we already have stored + val xp = storedXp + + // 2. Figure out how much XP we need to take to get to `xpCapacity` + val extraXpNeeded = xpCapacity - xp + + // 3. Take as much XP from the player as we can to get there + val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) + event.player.giveExp(-xpToTake) + + // 4. Set the new stored XP amount + storedXp = xp + xpToTake + } + } + + ... + } + ``` + +The second thing we should do is reuse NamespacedKeys. This is more of a 'best practice' thing - it's generally recommend to reuse keys. It'll become more apparent why later on. + +=== "Java" + ```java title="BaguetteOfWisdom.java" hl_lines="2 8 16" + public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { + public static final NamespacedKey STORED_XP_KEY = new NamespacedKey(MyAddon.getInstance(), "stored_xp"); + + ... + + public void setStoredXp(int xp) { + getStack().editPersistentDataContainer(pdc -> pdc.set( + STORED_XP_KEY, + PylonSerializers.INTEGER, + xp + )); + } + + public int getStoredXp() { + return getStack().getPersistentDataContainer().get( + STORED_XP_KEY, + PylonSerializers.INTEGER + ); + } + ``` +=== "Kotlin" + ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2-4 10 16" + class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { + companion object { + val STORED_XP_KEY = NamespacedKey(MyAddon.instance, "stored_xp") + } + + ... + + var storedXp: Int + get() = stack.persistentDataContainer.get( + STORED_XP_KEY, + PylonSerializers.INTEGER + )!! + set(value) { + stack.editPersistentDataContainer { pdc -> + pdc.set( + STORED_XP_KEY, + PylonSerializers.INTEGER, + value + ) + } + } + ``` + + + +=== "Java" + ```java title="MyAddon.java" hl_lines="3" + ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc(pdc -> pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + )) + .build(); + ``` +=== "Kotlin" + ```kotlin title="MyAddon.kt" hl_lines="4" + val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + .editPdc { pdc -> + pdc.set( + BaguetteOfWisdom.STORED_XP_KEY, + PylonSerializers.INTEGER, + 0 + ) + } + .build() + ``` + +And that's it! + +[PlayerInteractEntityEvent]: https://jd.papermc.io/paper/1.21.8/org/bukkit/event/player/PlayerInteractEntityEvent.html +[PylonItemEntityInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonItemEntityInteractor.html +[Persistent data container]: https://docs.papermc.io/paper/dev/pdc/ +[advanced lore]: advanced-lore.md +[PylonInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonInteractor.html diff --git a/docs/en/creating-addons/custom-items/adding-a-custom-item.md b/docs/en/creating-addons/custom-items/adding-a-custom-item.md index 3e3e7b5..b8c7349 100644 --- a/docs/en/creating-addons/custom-items/adding-a-custom-item.md +++ b/docs/en/creating-addons/custom-items/adding-a-custom-item.md @@ -2,14 +2,12 @@ ## Overview -So far, we've created a 'custom' item in the sense that we've changed the amount of hunger filled up by the baguette from 5 to 6. But what if we want to, for example, be able to right click entities with the baguette to set them on fire? There's no inbuilt way to do this like there was with food. We'll have to write some code to do this for us. +This section describes how to attach custom behaviour to an item. We'll demonstrate this by creating a baguette flamethrower which sets entities on fire when they are right clicked. --- ## The baguette flamethrower -To illustrate this, let's create a new 'baguette flamethrower' item. - ### Creating a 'normal item' Let's start by making a 'normal' item, like we did in the previous chapter. @@ -43,7 +41,7 @@ item: Next, we'll add the code to set entities on fire. -In order to do this, we can create a custom `BaguetteFlamethrower` class. All Pylon item classes must extend [PylonItem]. +In order to do this, we must create a custom `BaguetteFlamethrower` class. All Pylon item classes must extend [PylonItem]. === "Java" Create a new file `BaguetteFlamethrower.java` and add the following: @@ -62,7 +60,13 @@ In order to do this, we can create a custom `BaguetteFlamethrower` class. All Py class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack) ``` -We now want to do something whenever the player right clicks on a block while holding the baguette flamethrower. In order to do this, we can implement [PylonItemEntityInteractor] interface. This is a builtin Pylon interface with one method: `onUsedToRightClickEntity`. +We now want to run some code when the player right clicks on an entity while holding the baguette flamethrower. In order to do this, we need to implement a Pylon item interface. + +Pylon item interfaces are just interfaces that add different behaviours to items, like running some code when the item is used. + +Here, we need to implement the [PylonItemEntityInteractor] interface. This interface has one method: `onUsedToRightClickEntity`. + +!!! note You can find a full list of item interfaces [here](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/package-summary.html). === "Java" ```java title="BaguetteFlamethrower" hl_lines="6-9" diff --git a/docs/en/creating-addons/custom-items/advanced-lore.md b/docs/en/creating-addons/custom-items/advanced-lore.md index 7231c0f..c4ebd3b 100644 --- a/docs/en/creating-addons/custom-items/advanced-lore.md +++ b/docs/en/creating-addons/custom-items/advanced-lore.md @@ -1,16 +1,8 @@ # Advanced lore -## Overview - -Let's have a look at some of the other things we can do with item lore. Right now, it's very boring: - -![Boring baguette flamethrower lore](img/boring-baguette-flamethrower.png) - ---- - ## Minimessage -Lore isn't just plain text - there are **tags** like `` and `` and ``. These tags are added by [MiniMessage](https://docs.advntr.dev/minimessage/index.html). +Lore can contain **tags** like `` and `` and ``. These tags are added by [MiniMessage](https://docs.advntr.dev/minimessage/index.html). !!! info "You can find a full list of MiniMessage tags [here](https://docs.advntr.dev/minimessage/format.html)." @@ -69,7 +61,7 @@ baguette_flamethrower: ## Pylon's custom tags -The fun doesn't end there - Pylon adds its own MiniMessage tags! You've already met the `` tag added by Pylon, which is used all over the place. There are two other very important tags that Pylon adds: `` (instruction) and `` (attribute). +The fun doesn't end there - Pylon adds its own MiniMessage tags! You've already met the `` tag added by Pylon. There are two other very important tags that Pylon adds: `` (instruction) and `` (attribute). !!! info "You can find a full list of Pylon's custom tags [here](TODO)." @@ -101,7 +93,7 @@ baguette_flamethrower: ``` !!! success "Best practice" - Put description first, then instructions, then attributes. This helps keep things consistent across all of Pylon's items. + Put description first, then instructions, then attributes, as shown in the above example. This helps keep things consistent across all of Pylon's items. ![formatting](img/attribute.png) @@ -120,7 +112,7 @@ The `PylonItem` class has a method called `getPlaceholders`. This method - when === "Java" ```java title="BaguetteFlamethrower.java" hl_lines="13-18" public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { - private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", ConfigAdapter.INT); public BaguetteFlamethrower(@NotNull ItemStack stack) { super(stack); @@ -142,7 +134,7 @@ The `PylonItem` class has a method called `getPlaceholders`. This method - when === "Kotlin" ```kotlin title="BaguetteFlamethrower.kt" hl_lines="8-9" class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { - private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", ConfigAdapter.INT) override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { event.rightClicked.fireTicks = burnTimeTicks @@ -170,12 +162,12 @@ baguette_flamethrower: ### Units -One final thing. We're currently manually adding 'seconds' - but Pylon has a units API we can use. This API can automatically choose how to format the unit. It's very simple to use: +In the example above, we're manually adding 'seconds' - but Pylon has a units API we can use. This API can automatically choose how to format the unit. It's very simple to use: === "Java" ```java title="BaguetteFlamethrower.java" hl_lines="16" public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { - private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", ConfigAdapter.INT); public BaguetteFlamethrower(@NotNull ItemStack stack) { super(stack); @@ -197,7 +189,7 @@ One final thing. We're currently manually adding 'seconds' - but Pylon has a uni === "Kotlin" ```kotlin title="BaguetteFlamethrower.kt" hl_lines="9" class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { - private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", ConfigAdapter.INT) override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { event.rightClicked.fireTicks = burnTimeTicks @@ -222,4 +214,4 @@ baguette_flamethrower: !!! info "You can find a full list of Pylon's default units [here](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/util/gui/unit/UnitFormat.html)." !!! success "Best practice" - **Use units instead of hardcoding values**, even when it's a simple unit. This means the same unit looks the same across all items, and all values are formatted in the same way. For example, if we hardcoded placeholders, we might end up with '2.0' in some places and '2' in some places depending on whether we use a `float` or `int` - but using the unit API ensures it's always the same. + **Use the unit system instead of hardcoding in units**. This means the same unit looks the same across all items, and all values are formatted in the same way. diff --git a/docs/en/creating-addons/custom-items/full-code.md b/docs/en/creating-addons/custom-items/full-code.md index 0df8f21..f4f0394 100644 --- a/docs/en/creating-addons/custom-items/full-code.md +++ b/docs/en/creating-addons/custom-items/full-code.md @@ -19,12 +19,12 @@ BasePages.FOOD.addItem(baguetteFlamethrowerKey) ``` -[](tab break) + === "Java" ```java title="BaguetteFlamethrower.java" public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { - private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", ConfigAdapter.INT); public BaguetteFlamethrower(@NotNull ItemStack stack) { super(stack); @@ -46,7 +46,7 @@ === "Kotlin" ```kotlin title="BaguetteFlamethrower.kt" class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { - private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", ConfigAdapter.INT) override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { event.rightClicked.fireTicks = burnTimeTicks @@ -54,8 +54,9 @@ override fun getPlaceholders() = listOf(PylonArgument.of("burn-time", UnitFormat.SECONDS.format(burnTimeTicks / 20.0))) - } - ``` + }``` + + ```yaml title="en.yml" baguette_flamethrower: @@ -99,14 +100,14 @@ baguette_flamethrower: BasePages.FOOD.addItem(baguetteOfWisdomKey) ``` -[](tab break) + === "Java" ```java title="BaguetteOfWisdom.java" public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { public static final NamespacedKey STORED_XP_KEY = new NamespacedKey(MyAddon.getInstance(), "stored_xp"); - private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); + private final int xpCapacity = getSettings().getOrThrow("xp-capacity", ConfigAdapter.INT); public BaguetteOfWisdom(@NotNull ItemStack stack) { super(stack); @@ -170,7 +171,7 @@ baguette_flamethrower: val STORED_XP_KEY = NamespacedKey(MyAddon.getInstance(), "stored_xp") } - private val xpCapacity = settings.getOrThrow("xp-capacity", Int::class.java) + private val xpCapacity = settings.getOrThrow("xp-capacity", ConfigAdapter.INT) override fun getPlaceholders() = listOf( @@ -218,8 +219,9 @@ baguette_flamethrower: ) } } - } - ``` + }``` + + ```yaml title="en.yml" item: diff --git a/docs/en/creating-addons/custom-items/making-items-configurable.md b/docs/en/creating-addons/custom-items/making-items-configurable.md index feccbe3..ee0c302 100644 --- a/docs/en/creating-addons/custom-items/making-items-configurable.md +++ b/docs/en/creating-addons/custom-items/making-items-configurable.md @@ -2,13 +2,7 @@ ## Overview -Let's add a config so server owners can change how long the baguette flamethrower sets entities on fire for. - ---- - -## Creating the config file - -If you look inside your server's Pylon settings directory (`run/plugins/PylonCore/settings/pylonbase`) you'll see that every item and every block has its own config file. +Items can have configs associated with them. If you look inside your server's Pylon settings directory (`run/plugins/PylonCore/settings/pylonbase`) you'll see that every item and every block has its own config file. For example, `bandage.yml` contains: @@ -17,7 +11,11 @@ consume-seconds: 1.25 heal-amount: 4.0 ``` -We want to add our own config file which lets you choose how long the baguette flamethrower sets entities on fire for. +--- + +## Creating the config file + +Suppose we want to add our own config file which lets you choose how long the baguette flamethrower sets entities on fire for. Create a new file `resources/settings/baguette_flamethrower.yml` and add the following: @@ -28,18 +26,18 @@ burn-time-ticks: 40 !!! success "Best practice" When your config option is a quantity, include the unit in the name. If we just called it `burn-time`, users might not know that it's in ticks, and assume it's in seconds or something else. -Pylon will automatically copy all of your settings files into `run/plugins/PylonCore/settings/` when the settings file is first used. +Pylon will automatically copy all of your settings files into `run/plugins/PylonCore/settings/` when the settings file is first used, so that server owners can configure items. --- -## Using the config file +## Reading the config file -To read the `burn-time-ticks` value from the config file, we can use `getSettings().getOrThrow(...)`: +To read the `burn-time-ticks` value from the config file, we can use `getSettings().getOrThrow(...)`. `getSettings()` will just return the config with the same name as the item key - in this case `flamethrower_baguette`: === "Java" ```java title="BaguetteFlamethrower.java" hl_lines="2" public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { - private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", ConfigAdapter.INT); public BaguetteFlamethrower(@NotNull ItemStack stack) { super(stack); @@ -51,30 +49,33 @@ To read the `burn-time-ticks` value from the config file, we can use `getSetting } } ``` - !!! question "How does Pylon know which config file to use?" - Config files are actually per **key**, not per item. When calling `getSettings()`, all this does is take the item's key and find the settings file for it! It's just a shorthand for `Settings.get(getKey())`. If you used `Settings.get(new NamespacedKey(MyAddon.getInstance(), "buffoon"))` then the `buffoon.yml` settings file would be read instead. -=== "Kotlin" + === "Kotlin" ```kotlin title="BaguetteFlamethrower.kt" hl_lines="2" class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { - private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", ConfigAdapter.INT) override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { event.rightClicked.fireTicks = 40 } } ``` - !!! question "How does Pylon know which config file to use?" - Config files are actually per **key**, not per item. When accessing `settings`, all this does is take the item's key and find the settings file for it! It's just a shorthand for `Settings.get(key)`. If you used `Settings.get(NamespacedKey(MyAddon.instance, "buffoon"))` then the `buffoon.yml` settings file would be read instead. +!!! question "How does Pylon know which config file to use?" + Config files are actually per **key**, not per item. When accessing `settings`, all this does is take the item's key and find the settings file for it! It's just a shorthand for `Settings.get(key)`. If you used `Settings.get(NamespacedKey(MyAddon.instance, "buffoon"))` then the `buffoon.yml` settings file would be read instead. + +Settings also have a `get` method. This method just returns null if the key was not found. If you use `getOrThrow`, a nice exception will be thrown with some information on what key is missing and where. + +When calling `get` or `getOrThrow`, we have to supply a ConfigAdapter. This is a class that contains instructions on how to exactly read a value from a config file. + +!!! You can find a list of config adapters [here](TODO) -!!! question "Why are we using `getOrThrow` instead of `get`?" - Settings also have a `get` method. This method just returns null if the key was not found, so it should only be used where you don't necessarily *need* the key. If you use `getOrThrow`, a nice exception will be thrown with some information on what key is missing and where. +## Using the values we read from the config file And now we can use that value when setting entities on fire! === "Java" ```java title="BaguetteFlamethrower.java" hl_lines="10" public class BaguetteFlamethrower extends PylonItem implements PylonItemEntityInteractor { - private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", Integer.class); + private final int burnTimeTicks = getSettings().getOrThrow("burn-time-ticks", ConfigAdapter.INT); public BaguetteFlamethrower(@NotNull ItemStack stack) { super(stack); @@ -89,7 +90,7 @@ And now we can use that value when setting entities on fire! === "Kotlin" ```kotlin title="BaguetteFlamethrower.kt" hl_lines="5" class BaguetteFlamethrower(stack: ItemStack) : PylonItem(stack), PylonItemEntityInteractor { - private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", Int::class.java) + private val burnTimeTicks = settings.getOrThrow("burn-time-ticks", ConfigAdapter.INT) override fun onUsedToRightClickEntity(event: PlayerInteractEntityEvent) { event.rightClicked.fireTicks = burnTimeTicks @@ -97,7 +98,7 @@ And now we can use that value when setting entities on fire! } ``` -Try changing the config value, and the burn time should change with it. +If you change the config value, the burn time should change with it. !!! danger - The run server task deletes everything inside the `plugins` folder whenever it's run, meaning any changes you make to your new config there will be overwritten. Instead, just try changing the default value - or run the server manually so the `plugins` folder isn't deleted when the server restarts. + When testing configs, keep in mind that the run server task deletes everything inside the `plugins` folder whenever it's run, meaning any changes you make to your new config there will be overwritten. Instead, just try changing the default value - or run the server manually so the `plugins` folder isn't deleted when the server restarts. diff --git a/docs/en/creating-addons/custom-items/persistent-data.md b/docs/en/creating-addons/custom-items/persistent-data.md index 1457022..3ca5f5f 100644 --- a/docs/en/creating-addons/custom-items/persistent-data.md +++ b/docs/en/creating-addons/custom-items/persistent-data.md @@ -28,735 +28,119 @@ But suppose we want to store the charge level of a portable battery. If we can't [Persistent data container]s (PDCs) are a way to persistently store arbitrary data on an item. You can think of them as a similar sort of thing to YAML. You can 'set' keys and you can 'get' keys, and the keys can have different kinds of data - like strings, ints, or even other PDCs. -Take our example of keeping track of the charge level of a portable battery. We can store the charge level in the `charge_level` key in the item's PDC. It will then be saved when the item is put in a chest, or when the server restarts. +Take the example of keeping track of the charge level of a portable battery. We can store the charge level in the `charge_level` key in the item's PDC. It will then be saved when the item is put in a chest, or when the server restarts. If this is all a bit confusing - don't worry, an example should make it clearer. --- -## The Baguette of Wisdom +## The Counting Baguette ### The idea -[Studies have shown](https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ) that those who eat baguettes have a 68% higher IQ on average. Let's use this as inspiration for our addon. We'll create a 'Baguette of Wisdom' which allows you to transfer experience from one player to another. - -Here's the plan: - -- The baguette has a maximum XP capacity -- You can right click with the baguette to 'charge' it with experience -- You can shift right click with the baguette to discharge the experience - -To do this, we're going to need to keep track of how much experience the baguette has inside of it. +Let's create a 'Counting Baguette' that stores a number. When right clicked, the baguette will send the stored number to you in chat and increment it. ### Creating the item -You know the drill from last time: +We'll start by creating a new baguette that doesn't count anything yet. === "Java" ```java title="MyAddon.java" - NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); - ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + NamespacedKey countingBaguetteKey = new NamespacedKey(this, "counting_baguette"); + ItemStack countingBaguette = ItemStackBuilder.pylonItem(Material.BREAD, countingBaguetteKey) .build(); - PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); - BasePages.FOOD.addItem(baguetteOfWisdomKey); + PylonItem.register(CountingBaguette.class, countingBaguette); + BasePages.FOOD.addItem(countingBaguette); ``` === "Kotlin" ```kotlin title="MyAddon.kt" - val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") - val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) + val countingBaguetteKey = NamespacedKey(this, "counting_baguette") + val countingBaguette = ItemStackBuilder.pylonItem(Material.BREAD, countingBaguetteKey) .build() - PylonItem.register(baguetteOfWisdom) - BasePages.FOOD.addItem(baguetteOfWisdomKey) + PylonItem.register(countingBaguette) + BasePages.FOOD.addItem(countingBaguette) ``` -[](this invisible link is important to break up the two code tabs above and below, otherwise the tabs will all be on one line) + === "Java" - ```java title="BaguetteOfWisdom.java" - public class BaguetteOfWisdom extends PylonItem { - public BaguetteOfWisdom(@NotNull ItemStack stack) { + ```java title="CountingBaguette.java" + public class CountingBaguette extends PylonItem { + public CountingBaguette(@NotNull ItemStack stack) { super(stack); } } ``` === "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) + ```kotlin title="CountingBaguette.kt" + class CountingBaguette(stack: ItemStack) : PylonItem(stack) ``` ```yaml title="en.yml" item: - baguette_of_wisdom: - name: "Baguette of Wisdom" + counting_baguette: + name: "Counting Baguette" lore: |- - Use the power of baguettes to transfer XP + Mathematics is improved tenfold when combined with baguettes ``` -### Adding a config for the XP capacity - -Now, let's add a config value for the max XP capacity: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="2" - public class BaguetteOfWisdom extends PylonItem { - private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); - - public BaguetteOfWisdom(@NotNull ItemStack stack) { - super(stack); - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { - private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) - } - ``` - -```yaml title="baguette_of_wisdom.yml" -xp-capacity: 200 -``` - -We'll be using this later. - -### Improving the lore - -Let's add some instructions to the lore, and attributes for the max charge and current charge: - -```yaml title="en.yml" hl_lines="6-9" -item: - baguette_of_wisdom: - name: "Baguette of Wisdom" - lore: |- - Use the power of baguettes to transfer XP - Right click to charge with XP - Shift right click to discharge stored XP - XP capacity: %xp_capacity% - Stored XP: %stored_xp% -``` +### Adding the counting mechanic -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="8-14" - public class BaguetteOfWisdom extends PylonItem { - private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); - - public BaguetteOfWisdom(@NotNull ItemStack stack) { - super(stack); - } - - @Override - public @NotNull List getPlaceholders() { - return List.of( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) - // TODO add stored_xp placeholder - ); - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-7" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack) { - private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) - - override fun getPlaceholders() = listOf( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) - // TODO add stored_xp placeholder - ) - } - ``` - -All of this should be familiar from the [advanced lore] section. - -### The charge/discharge mechanic - -Next, let's allow the player to charge by right clicking, and discharge by shift right clicking. - -We can use the [PylonInteractor] class to do this: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="1 16-23" - public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { - private final int xpCapacity = getSettings().getOrThrow("xp-capacity", Integer.class); - - public BaguetteOfWisdom(@NotNull ItemStack stack) { - super(stack); - } - - @Override - public @NotNull List getPlaceholders() { - return List.of( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) - // TODO add stored_xp placeholder - ); - } - - @Override - public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { - if (event.getPlayer().isSneaking()) { - // TODO discharge logic - } else { - // TODO charge logic - } - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="1 9-15" - - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { - private val xpCapacity: Int = settings.getOrThrow("xp-capacity", Int::class.java) - - override fun getPlaceholders() = listOf( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)) - // TODO add stored_xp placeholder - ) - - override fun onUsedToRightClick(event: PlayerInteractEvent) { - if (event.player.isSneaking) { - // TODO discharge logic - } else { - // TODO charge logic - } - } +First, we need to detect when the player right clicks: +```java title="CountingBaguette.java" hl_lines="1 6-10" +public class CountingBaguette extends PylonItem implements PylonInteractor { + public CountingBaguette(@NotNull ItemStack stack) { + super(stack); } - ``` - -### The charge logic -Let's now do the **charge** logic. In order to charge a Baguette of Wisdom, we need to store its charge level. As mentioned beforehand, we can use the item's [persistent data container] to do this. To start with, let's just set the charge level to 50: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="6-10" @Override public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { - if (event.getPlayer().isSneaking()) { - // TODO discharge logic - } else { - getStack().editPersistentDataContainer(pdc -> pdc.set( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER, - 50 - )); - } + // TODO send stored value to player + // TODO increment stored value } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-11" - override fun onUsedToRightClick(event: PlayerInteractEvent) { - if (event.player.isSneaking) { - // TODO discharge logic - } else { - stack.editPersistentDataContainer { pdc -> - pdc.set( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER, - 50 - ) - } - } - } - ``` - -As you can see, we need to provide three things to set a PDC value: the **key**, the **serializer**, and the **value**. +} +``` -The serializer is just a 'thing' that describes how to convert your type into a more primitive type that can be stored on disk - we won't go into details. You need a serializer for every type that you want to store - so you can't store, for example, `MyAddon` in a persistent data container as there is no serializer for it and it doesn't make sense to create one anyway. +Inside the function, we need to read the stored value, send it to the player, and then store a value that is one higher. -!!! info "You can find a full list of serializers [here](https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.html)" +To store a piece of data in a persistent data container, we need two other things: a **namespaced key** by which the piece of data is identified, and the **type** of the piece of data in the form of a [PersistentDataType]. -Ok. But what we really need to do is 'top up' the stored xp using the player's experience: +!!! info "PylonSerializers" + For convenience, Pylon supplies a range of types in the [PylonSerializers] file. This includes all the default types provided by Paper, but also some extra types (like UUID, Vector, BlockPosition, ItemStack, and more). -1. Read how much XP we already have stored -2. Figure out how much XP we need to take to get to `xpCapacity` -3. Take as much XP from the player as we can to get there -4. Set the new XP amount +```java title="CountingBaguette.java" hl_lines="2 10-12" +public class CountingBaguette extends PylonItem implements PylonInteractor { + public static final NamespacedKey COUNT_KEY = new NamespacedKey(MyAddon.getInstance(), "count"); -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="6-24" - @Override - public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { - if (event.getPlayer().isSneaking()) { - // TODO discharge logic - } else { - // 1. Read how much XP we already have stored - int xp = getStack().getPersistentDataContainer().get( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER - ); - - // 2. Figure out how much XP we need to take to get to `xpCapacity` - int extraXpNeeded = xpCapacity - xp; - - // 3. Take as much XP from the player as we can to get there - int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); - event.getPlayer().giveExp(-xpToTake); - - // 4. Set the new stored XP amount - getStack().editPersistentDataContainer(pdc -> pdc.set( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER, - xp + xpToTake - )); - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="5-25" - override fun onUsedToRightClick(event: PlayerInteractEvent) { - if (event.player.isSneaking) { - // TODO discharge logic - } else { - // 1. Read how much XP we already have stored - val xp = stack.persistentDataContainer.get( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER - )!! - - // 2. Figure out how much XP we need to take to get to `xpCapacity` - val extraXpNeeded = xpCapacity - xp - - // 3. Take as much XP from the player as we can to get there - val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) - event.player.giveExp(-xpToTake) - - // 4. Set the new stored XP amount - stack.editPersistentDataContainer { pdc -> - pdc.set( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER, - xp + xpToTake - ) - } - } + public CountingBaguette(@NotNull ItemStack stack) { + super(stack); } - ``` - -### The discharge logic - -And now for the discharge logic, which is quite similar: -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="4-18" @Override public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { - if (event.getPlayer().isSneaking()) { - // 1. Read how much XP we have stored - int xp = getStack().getPersistentDataContainer().get( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER - ); - - // 2. Give all the XP to the player - event.getPlayer().giveExp(xp); - - // 3. Set the stored XP to 0 - getStack().editPersistentDataContainer(pdc -> pdc.set( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER, - 0 - )); - } else { - ... - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-19" - override fun onUsedToRightClick(event: PlayerInteractEvent) { - if (event.player.isSneaking) { - // 1. Read how much XP we have stored - val xp = stack.persistentDataContainer.get( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER - )!! - - // 2. Give all the XP to the player - event.player.giveExp(xp) - - // 3. Set the stored XP to 0 - stack.editPersistentDataContainer { pdc -> - pdc.set( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER, - 0 - ) - } - } else { - ... - } - } - ``` - -### Adding a placeholder - -Finally, let's add in the placeholder for the stored charge: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="5-10" - @Override - public @NotNull List getPlaceholders() { - return List.of( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), - PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( - getStack().getPersistentDataContainer().get( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER - )) - ) - ); + int count = getStack().getPersistentDataContainer().get(COUNT_KEY, PylonSerializers.INTEGER); + event.getPlayer().sendMessage(String.valueOf(count)); + getStack().editPersistentDataContainer(pdc -> pdc.set(COUNT_KEY, PylonSerializers.INTEGER, count + 1)); } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="3-8" - override fun getPlaceholders() = listOf( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), - PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format( - stack.persistentDataContainer.get( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER - )!! - )) - ) - ``` - -### Testing it out - -Now let's start the server and test our glorious new item. Try giving yourself a Baguette of Wisdom. - -Obviously, as I am an expert programmer, it will work perfectly the first time and nothing will go wro- *huh*? - -![Baguette of wisdom error](img/baguette_of_wisdom_error.png) - -What nonsense is this? Have the brits sabotaged us again?! - -When something like this happens, your first port of call should always be the server console to see if any errors have been logged. And indeed, if you have a look in the console you should find the following error: - -```title="console" -[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null -[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) -[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.ItemButton.getItemProvider(ItemButton.kt:47) -[12:51:22 WARN]: at xyz.xenondevs.invui.gui.SlotElement$ItemSlotElement.getItemStack(SlotElement.java:44) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.redrawItem(AbstractWindow.java:109) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.initItems(AbstractSingleWindow.java:58) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.open(AbstractWindow.java:279) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow$AbstractBuilder.open(AbstractWindow.java:679) -[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.pages.base.GuidePage.open(GuidePage.kt:28) -[12:51:22 WARN]: at pylon-core-0.11.2.jar//io.github.pylonmc.pylon.core.guide.button.PageButton.handleClick(PageButton.kt:38) -[12:51:22 WARN]: at xyz.xenondevs.invui.gui.AbstractGui.handleClick(AbstractGui.java:95) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractSingleWindow.handleClick(AbstractSingleWindow.java:84) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.AbstractWindow.handleClickEvent(AbstractWindow.java:199) -[12:51:22 WARN]: at xyz.xenondevs.invui.window.WindowManager.handleInventoryClick(WindowManager.java:117) -[12:51:22 WARN]: at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:80) -[12:51:22 WARN]: at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:71) -[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) -[12:51:22 WARN]: at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) -[12:51:22 WARN]: at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:628) -[12:51:22 WARN]: at net.minecraft.server.network.ServerGamePacketListenerImpl.handleContainerClick(ServerGamePacketListenerImpl.java:3208) -[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:59) -[12:51:22 WARN]: at net.minecraft.network.protocol.game.ServerboundContainerClickPacket.handle(ServerboundContainerClickPacket.java:14) -[12:51:22 WARN]: at net.minecraft.network.protocol.PacketUtils.lambda$ensureRunningOnSameThread$0(PacketUtils.java:29) -[12:51:22 WARN]: at net.minecraft.server.TickTask.run(TickTask.java:18) -[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.doRunTask(BlockableEventLoop.java:155) -[12:51:22 WARN]: at net.minecraft.util.thread.ReentrantBlockableEventLoop.doRunTask(ReentrantBlockableEventLoop.java:24) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:1450) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.doRunTask(MinecraftServer.java:176) -[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.pollTask(BlockableEventLoop.java:129) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTaskInternal(MinecraftServer.java:1430) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.pollTask(MinecraftServer.java:1424) -[12:51:22 WARN]: at net.minecraft.util.thread.BlockableEventLoop.managedBlock(BlockableEventLoop.java:139) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.managedBlock(MinecraftServer.java:1381) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.waitUntilNextTick(MinecraftServer.java:1389) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1266) -[12:51:22 WARN]: at net.minecraft.server.MinecraftServer.lambda$spin$2(MinecraftServer.java:310) +} ``` -Wow. That's a fat error. But the lines we are most interested in are right at the top: - -```title="console" -[12:51:22 WARN]: java.lang.NullPointerException: Cannot invoke "java.lang.Float.floatValue()" because the return value of "io.papermc.paper.persistence.PersistentDataContainerView.get(org.bukkit.NamespacedKey, org.bukkit.persistence.PersistentDataType)" is null -[12:51:22 WARN]: at my-addon-MODIFIED-1757418660070.jar//io.github.pylonmc.myaddon.BaguetteOfWisdom.getPlaceholders(BaguetteOfWisdom.java:28) -``` - -So it looks like the error occurred on line 28, in the `getPlaceholders` function, where we try to read from the persistent data container. Apparently, the 'stored_xp' key couldn't be found in the PDC, because the call to `getStack().getPersistentDataContainer().get(...)` returned null. - -!!! question "Wait, why did `getPlaceholders` get called and error *now*? We haven't given ourselves the item yet." - Simply put, the guide also needs to call `getPlaceholders` to display the item to you. The error only appears once you open the guide - or once you give yourself the item with `/py give`. - -!!! Note "Null safety" - If you've been following along in Kotlin, you may have noticed that your error is different from the one above. Additionally, if you played around with the code a bit, you may have noticed that the Kotlin code refuses to compile unless you add a `!!` after the call to `get(...)` in the `getPlaceholders` function. This is because Kotlin actually tracks nulls in the type system, and will error during compile time instead of run time if you try to use a potentially null value without checking for null first. The `!!` tells the compiler, "hey, I know what I'm doing, this value will never be null." This is one of the advantages of using Kotlin over Java, as it can help catch potential null pointer exceptions before they even happen, and is one of the reasons why Pylon Core is written in Kotlin. In this situation, an Elvis operator (`?:`) or a call to `getOrDefault` would have been more appropriate, but for the purposes of this tutorial, we will leave it as is. - -This actually makes perfect sense if you think about it. At no point do we set a default value for the stored XP, so of course any call to get it will return null. - -### Adding a default value - -To add a default value for stored XP to the PDC, we can modify the itemstack itself when we create it: - -=== "Java" - ```java title="MyAddon.java" hl_lines="3-7" - NamespacedKey baguetteOfWisdomKey = new NamespacedKey(this, "baguette_of_wisdom"); - ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) - .editPdc(pdc -> pdc.set( - new NamespacedKey(this, "stored_xp"), - PylonSerializers.INTEGER, - 0 - )) - .build(); - PylonItem.register(BaguetteOfWisdom.class, baguetteOfWisdom); - BasePages.FOOD.addItem(baguetteOfWisdomKey); - ``` -=== "Kotlin" - ```kotlin title="MyAddon.kt" hl_lines="3-9" - val baguetteOfWisdomKey = NamespacedKey(this, "baguette_of_wisdom") - val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) - .editPdc { pdc -> - pdc.set( - NamespacedKey(this, "stored_xp"), - PylonSerializers.INTEGER, - 0 - ) - } - .build() - PylonItem.register(baguetteOfWisdom) - BasePages.FOOD.addItem(baguetteOfWisdomKey) - ``` - -Now let's try again. - -![Baguette of wisdom success](img/baguette_of_wisdom_success.png) - -Ah, perfect! - -One last thing left to do... - -### Cleaning up - -The Baguette of Wisdom works, but there are some improvements we can make. - -First, we could pull out the get/set code into their own functions: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="4-10 12-17" - public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { - ... - - public void setStoredXp(int xp) { - getStack().editPersistentDataContainer(pdc -> pdc.set( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER, - xp - )); - } - - public int getStoredXp() { - return getStack().getPersistentDataContainer().get( - new NamespacedKey(MyAddon.getInstance(), "stored_xp"), - PylonSerializers.INTEGER - ); - } - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-17" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { - ... - - var storedXp: Int - get() = stack.persistentDataContainer.get( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER - )!! - set(value) { - stack.editPersistentDataContainer { pdc -> - pdc.set( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER, - value - ) - } - } - } - ``` - - !!! Note "Delegate" - Alternatively, Pylon provides a property delegate for persistent data values that can be used to simplify this even further: - - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="4-8" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { - ... - - var storedXp: Int by persistentData( - NamespacedKey(MyAddon.instance, "stored_xp"), - PylonSerializers.INTEGER, - 0 - ) - } - ``` - -And now, we can use these functions in the rest of the code, which is much cleaner: - -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="9 17 23 26 36" - public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { - - ... - - @Override - public @NotNull List getPlaceholders() { - return List.of( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), - PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(getStoredXp())) - ); - } - - @Override - public void onUsedToRightClick(@NotNull PlayerInteractEvent event) { - if (event.getPlayer().isSneaking()) { - // 1. Read how much XP we already have stored - int xp = getStoredXp(); - - // 2. Give all the XP to the player - event.getPlayer().giveExp(xp); - - // 3. Set the stored XP to 0 - setStoredXp(0); - } else { - // 1. Read how much XP we already have stored - int xp = getStoredXp(); - - // 2. Figure out how much XP we need to take to get to `xpCapacity` - int extraXpNeeded = xpCapacity - xp; - - // 3. Take as much XP from the player as we can to get there - int xpToTake = Math.min(event.getPlayer().calculateTotalExperiencePoints(), extraXpNeeded); - event.getPlayer().giveExp(-xpToTake); - - // 4. Set the new stored XP amount - setStoredXp(xp + xpToTake); - } - } - - ... - } - ``` -=== "Kotlin" - - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="7 13 19 22 32" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { - - ... - - override fun getPlaceholders() = listOf( - PylonArgument.of("xp_capacity", UnitFormat.EXPERIENCE.format(xpCapacity)), - PylonArgument.of("stored_xp", UnitFormat.EXPERIENCE.format(storedXp)) - ) - - override fun onUsedToRightClick(event: PlayerInteractEvent) { - if (event.player.isSneaking) { - // 1. Read how much XP we already have stored - val xp = storedXp - - // 2. Give all the XP to the player - event.player.giveExp(xp) - - // 3. Set the stored XP to 0 - storedXp = 0 - } else { - // 1. Read how much XP we already have stored - val xp = storedXp - - // 2. Figure out how much XP we need to take to get to `xpCapacity` - val extraXpNeeded = xpCapacity - xp - - // 3. Take as much XP from the player as we can to get there - val xpToTake = min(event.player.calculateTotalExperiencePoints(), extraXpNeeded) - event.player.giveExp(-xpToTake) - - // 4. Set the new stored XP amount - storedXp = xp + xpToTake - } - } - - ... - } - ``` +### Setting the default value -The second thing we should do is reuse NamespacedKeys. This is more of a 'best practice' thing - it's generally recommend to reuse keys. It'll become more apparent why later on. +You may have noticed that we have not set a starting value for the count yet. In order to do this, we need to edit the default item stack: -=== "Java" - ```java title="BaguetteOfWisdom.java" hl_lines="2 8 16" - public class BaguetteOfWisdom extends PylonItem implements PylonInteractor { - public static final NamespacedKey STORED_XP_KEY = new NamespacedKey(MyAddon.getInstance(), "stored_xp"); - - ... - - public void setStoredXp(int xp) { - getStack().editPersistentDataContainer(pdc -> pdc.set( - STORED_XP_KEY, - PylonSerializers.INTEGER, - xp - )); - } - - public int getStoredXp() { - return getStack().getPersistentDataContainer().get( - STORED_XP_KEY, - PylonSerializers.INTEGER - ); - } - ``` -=== "Kotlin" - ```kotlin title="BaguetteOfWisdom.kt" hl_lines="2-4 10 16" - class BaguetteOfWisdom(stack: ItemStack) : PylonItem(stack), PylonInteractor { - companion object { - val STORED_XP_KEY = NamespacedKey(MyAddon.instance, "stored_xp") - } - - ... - - var storedXp: Int - get() = stack.persistentDataContainer.get( - STORED_XP_KEY, - PylonSerializers.INTEGER - )!! - set(value) { - stack.editPersistentDataContainer { pdc -> - pdc.set( - STORED_XP_KEY, - PylonSerializers.INTEGER, - value - ) - } - } - ``` - -[](another tab break) - -=== "Java" - ```java title="MyAddon.java" hl_lines="3" - ItemStack baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) - .editPdc(pdc -> pdc.set( - BaguetteOfWisdom.STORED_XP_KEY, - PylonSerializers.INTEGER, - 0 - )) - .build(); - ``` -=== "Kotlin" - ```kotlin title="MyAddon.kt" hl_lines="4" - val baguetteOfWisdom = ItemStackBuilder.pylonItem(Material.BREAD, baguetteOfWisdomKey) - .editPdc { pdc -> - pdc.set( - BaguetteOfWisdom.STORED_XP_KEY, - PylonSerializers.INTEGER, - 0 - ) - } - .build() - ``` +```java title="CountingBaguette.java" hl_lines="3" +NamespacedKey countingBaguetteKey = new NamespacedKey(this, "counting_baguette"); +ItemStack countingBaguette = ItemStackBuilder.pylonItem(Material.BREAD, countingBaguetteKey) + .editPdc(pdc -> pdc.set(CountingBaguette.COUNT_KEY, PylonSerializers.INTEGER, 0)) + .build(); +PylonItem.register(CountingBaguette.class, countingBaguette); +BasePages.FOOD.addItem(countingBaguette); +``` -And that's it! +And that's it! The data we stored will persist as long as the item exists. -[PlayerInteractEntityEvent]: https://jd.papermc.io/paper/1.21.8/org/bukkit/event/player/PlayerInteractEntityEvent.html -[PylonItemEntityInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonItemEntityInteractor.html [Persistent data container]: https://docs.papermc.io/paper/dev/pdc/ -[advanced lore]: advanced-lore.md -[PylonInteractor]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/item/base/PylonInteractor.html +[PersistentDataType]: https://docs.papermc.io/paper/dev/pdc/#data-types +[PylonSerializers]: https://pylonmc.github.io/pylon-core/docs/javadoc/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.html + diff --git a/docs/en/creating-addons/the-basics/adding-a-recipe.md b/docs/en/creating-addons/the-basics/adding-a-recipe.md new file mode 100644 index 0000000..23060cc --- /dev/null +++ b/docs/en/creating-addons/the-basics/adding-a-recipe.md @@ -0,0 +1,44 @@ +# Adding a recipe + +## Overview + +Recipes are stored as YAML files in the `resources/recipes` folder of your addon. For example, in Pylon base, the recipe for tin nuggets is stored in `resources/recipes/minecraft/crafting_shapeless.yml` and looks like this: + +```yaml title="crafting_shapeless.yml" +pylonbase:tin_nuggets_from_tin_ingot: + ingredients: + - pylonbase:tin_ingot + result: + pylonbase:tin_nugget: 9 +``` + +Each recipe type has a specific YAML structure you must follow when writing recipes for it. + +!!! info Writing recipe configs + If you are not sure how to write a config for a specific type of recipe, look for YAML files corresponding to that recipe type. For example, you could look at the [`resources/recipes` folder of Pylon base](https://github.com/pylonmc/pylon-base/tree/master/src/main/resources/recipes/pylonbase) to see how to create a magic altar recipe. + +--- + +## Creating a recipe for the baguette + +Dough can be smelted into bread in a normal furnace or smoker. However, for the baguette, we need something more heavy duty: the blast furnace. + +Let's create a recipe that smelts dough into baguettes in a blast furnace. + +Pylon base already has [an example of how to write blasting recipes](https://github.com/pylonmc/pylon-base/blob/master/src/main/resources/recipes/minecraft/blasting.yml), so let's look there. + +Based off of that, let's create our recipe. First, create a file `resources/recipes/minecraft/blasting.yml`. Then, insert the following (and change `myaddon` to the key of your addon): +```yaml +pylonbase:baguette_blasting: + ingredient: pylonbase:dough + result: myaddon:baguette + experience: 0.5 + category: misc +``` + +The first line is the **key** of the recipe. All recipes of a given type must have a unique key. The rest of the recipe YAML is fairly self explanatory, except perhaps `category`, which is the category that the recipe should appear in in the crafting book. + +!!! info Finding item keys + If you're not sure what an item's key is, hold it in your hand and run `/py key`. + +!!! note "From now on, recipes will not be provided for any item examples for the sake of brevity." diff --git a/docs/en/creating-addons/the-basics/adding-a-research.md b/docs/en/creating-addons/the-basics/adding-a-research.md new file mode 100644 index 0000000..a854521 --- /dev/null +++ b/docs/en/creating-addons/the-basics/adding-a-research.md @@ -0,0 +1,54 @@ +# Adding a research + +## Overview + +Adding a research is even easier than adding a recipe. All of an addon's researches are stored in `resources/researches.yml`. For example, here is the 'improved drilling techniques' research: + +```yaml title="researches.yml" +... +improved_drilling_techniques: + material: iron_pickaxe + cost: 15 + unlocks: + - pylonbase:improved_manual_core_drill + - pylonbase:subsurface_core_chunk +... +``` + +Each research needs a corresponding language entry: +```yaml title="en.yml" +... +research: + ... + improved_drilling_techniques: "<#4f4641>Improved drilling techniques" + ... +``` + +--- + +## Adding a research for the baguette + +Let's create a research called 'Baguette Supremacy' that unlocks the baguette. + +First, create a file `resources/researches.yml`. Inside the file, paste the following (and change `myaddon` to your addon's key): +```yaml title="researches.yml" +baguette_supremacy: + material: bread + cost: 3 + unlocks: + - myaddon:baguette +``` + +Next, create a `researches` section in `en.yml` and add an entry for the research as follows: + +```yaml title="en.yml" +researches: + baguette_supremacy: "<#ff0000>BAGUETTE SUPREMACY" +``` + +!!! question "What's this `<#ff0000>` business?" + This is [hex color code](https://www.howtogeek.com/761277/what-is-a-hex-code-for-colors/) inside a minimessage tag. You can find out more in the [advanced lore](../custom-items/advanced-lore.md) section. + +That's it! + +!!! note "From now on, researches will not be provided for any item examples for the sake of brevity." diff --git a/docs/en/creating-addons/your-first-item/adding-an-item.md b/docs/en/creating-addons/the-basics/adding-an-item.md similarity index 94% rename from docs/en/creating-addons/your-first-item/adding-an-item.md rename to docs/en/creating-addons/the-basics/adding-an-item.md index ba2b023..ae9eb5a 100644 --- a/docs/en/creating-addons/your-first-item/adding-an-item.md +++ b/docs/en/creating-addons/the-basics/adding-an-item.md @@ -2,7 +2,7 @@ ## Overview -Our addon so far has only one class: `MyAddon` (or whatever you renamed it to). This class extends [JavaPlugin] and implements [PylonAddon]. There are some comments inside the class to explain what each part does - have a read through and try to understand how it works. Our addon doesn't actually do anything yet though - so let's add a new item! +The template addon has only one class: `MyAddon` (or whatever you renamed it to). This class extends [JavaPlugin] and implements [PylonAddon]. There are some comments inside the class to explain what each part does - have a read through and try to understand how it works. Our addon doesn't actually do anything yet though - so let's add a new item! Let's create a baguette, which fills 6 hunger bars, to start with. diff --git a/docs/en/creating-addons/your-first-item/adding-name-and-lore.md b/docs/en/creating-addons/the-basics/adding-name-and-lore.md similarity index 88% rename from docs/en/creating-addons/your-first-item/adding-name-and-lore.md rename to docs/en/creating-addons/the-basics/adding-name-and-lore.md index 7da9387..8b22f15 100644 --- a/docs/en/creating-addons/your-first-item/adding-name-and-lore.md +++ b/docs/en/creating-addons/the-basics/adding-name-and-lore.md @@ -1,6 +1,6 @@ # Using the language system -## What is the language system? +## What is a language system? 'Language system' might sound intimidating if you've never used one before, but it's very straightforward. A language system is just a way to make things translateable. @@ -41,11 +41,11 @@ Obviously, we'll need some system to substitute in the right translation for the --- -## Adding name and lore to our baguette +## Using Pylon's language system Remember how we did `item.setName("item.nuclear-bomb.name")` above? In Pylon, you don't need to do that because Pylon **automatically generates the translation key** based on your item's key. All we need to do is create translation files and make sure they contain the correct keys. -Open the 'en.yml' file ('en' is a code for 'English') in the `src/main/resources/lang` folder. +Let's do this for the Baguette (fom the previous section). Open the 'en.yml' file ('en' is a code for 'English') in the `src/main/resources/lang` folder. !!! Note "Adding translations for other languages" If we wanted to create a Spanish language file, we would call it 'es.yml' - or 'cs.yml' for Czech, and so on. See [this Wikipedia page](https://minecraft.wiki/w/Language) for a full list of these 2-letter codes. @@ -63,7 +63,7 @@ item: Note that we have an `addon` key. This is just the name of your addon. -We've also added `name` and `lore` for our baguette. Notice that we're using `baguette` here because that's the key that we created earlier, in this line: +We've also added `name` and `lore` for the baguette. Notice that we're using `baguette` here because that's the key that identifies the baguette, as we specified in this line: === "Java" ```java NamespacedKey baguetteKey = new NamespacedKey(this, "baguette"); diff --git a/docs/en/creating-addons/your-first-item/full-code.md b/docs/en/creating-addons/the-basics/full-code.md similarity index 87% rename from docs/en/creating-addons/your-first-item/full-code.md rename to docs/en/creating-addons/the-basics/full-code.md index 696b5c0..9ee105c 100644 --- a/docs/en/creating-addons/your-first-item/full-code.md +++ b/docs/en/creating-addons/the-basics/full-code.md @@ -1,6 +1,4 @@ -# The full code - -There's been a lot to go through, but when you look at the code we needed, it's actually really not too much: +# Full code === "Java" ```java title='MyAddon.java' @@ -21,6 +19,8 @@ There's been a lot to go through, but when you look at the code we needed, it's BasePages.FOOD.addItem(baguetteKey) ``` + + ```yaml title='en.yml' addon: "" diff --git a/docs/en/creating-addons/your-first-item/img/baguette-missing-translation-key.png b/docs/en/creating-addons/the-basics/img/baguette-missing-translation-key.png similarity index 100% rename from docs/en/creating-addons/your-first-item/img/baguette-missing-translation-key.png rename to docs/en/creating-addons/the-basics/img/baguette-missing-translation-key.png diff --git a/docs/en/creating-addons/your-first-item/img/baguette.png b/docs/en/creating-addons/the-basics/img/baguette.png similarity index 100% rename from docs/en/creating-addons/your-first-item/img/baguette.png rename to docs/en/creating-addons/the-basics/img/baguette.png diff --git a/docs/en/creating-addons/your-first-item/img/running-test-server.png b/docs/en/creating-addons/the-basics/img/running-test-server.png similarity index 100% rename from docs/en/creating-addons/your-first-item/img/running-test-server.png rename to docs/en/creating-addons/the-basics/img/running-test-server.png diff --git a/docs/en/creating-addons/your-first-item/practice-tasks.md b/docs/en/creating-addons/the-basics/practice-tasks.md similarity index 100% rename from docs/en/creating-addons/your-first-item/practice-tasks.md rename to docs/en/creating-addons/the-basics/practice-tasks.md diff --git a/docs/en/creating-addons/your-first-item/running-your-addon.md b/docs/en/creating-addons/the-basics/running-your-addon.md similarity index 89% rename from docs/en/creating-addons/your-first-item/running-your-addon.md rename to docs/en/creating-addons/the-basics/running-your-addon.md index c41fd52..a14f6f9 100644 --- a/docs/en/creating-addons/your-first-item/running-your-addon.md +++ b/docs/en/creating-addons/the-basics/running-your-addon.md @@ -17,9 +17,9 @@ Once the server has started, you can connect on `localhost:25565`. Make sure you --- -## Getting the item +## Testing the baguette -Now, you can give yourself your item. You can do this using `/py give`. For example, `/py give Idra my-addon:baguette`. If you've done everything right, you should receive your new baguette. +Now, you can give yourself the Baguette from the previous section. You can do this using `/py give`. For example, `/py give Idra my-addon:baguette`. If you've done everything right, you should receive your new baguette. But wait... diff --git a/mkdocs.yml b/mkdocs.yml index 78e650f..2abf142 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,17 +37,20 @@ nav: - Commands & permissions: installation/commands-and-permissions.md - Creating addons: - Getting started: creating-addons/getting-started.md - - Your first item: - - Adding an item: creating-addons/your-first-item/adding-an-item.md - - Running your addon: creating-addons/your-first-item/running-your-addon.md - - Adding name & lore: creating-addons/your-first-item/adding-name-and-lore.md - - Full code: creating-addons/your-first-item/full-code.md - - Practice tasks: creating-addons/your-first-item/practice-tasks.md + - The basics: + - Adding an item: creating-addons/the-basics/adding-an-item.md + - Running your addon: creating-addons/the-basics/running-your-addon.md + - Adding name & lore: creating-addons/the-basics/adding-name-and-lore.md + - Adding a recipe: creating-addons/the-basics/adding-a-recipe.md + - Adding a research: creating-addons/the-basics/adding-a-research.md + - Full code: creating-addons/the-basics/full-code.md + - Practice tasks: creating-addons/the-basics/practice-tasks.md - Custom items: - Adding a custom item: creating-addons/custom-items/adding-a-custom-item.md - Making items configurable: creating-addons/custom-items/making-items-configurable.md - Advanced lore: creating-addons/custom-items/advanced-lore.md - Persistent data: creating-addons/custom-items/persistent-data.md + - A complete example: creating-addons/custom-items/a-complete-example.md - Full code: creating-addons/custom-items/full-code.md - Practice tasks: creating-addons/custom-items/practice-tasks.md - Reference: