Repository: Darkhax-Minecraft/Bookshelf Branch: 1.21.1 Commit: 5dead2485866 Files: 225 Total size: 544.4 KB Directory structure: gitextract_et8g_e68/ ├── .gitattributes ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md ├── build.gradle ├── buildSrc/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── groovy/ │ ├── build-number.gradle │ ├── git-changelog.gradle │ ├── minify-json.gradle │ ├── multiloader-common.gradle │ ├── multiloader-loader.gradle │ ├── patreon.gradle │ ├── project_validation.gradle │ ├── readme-update.gradle │ ├── secret-loader.gradle │ └── version-checker.gradle ├── common/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── net/ │ │ └── darkhax/ │ │ └── bookshelf/ │ │ └── common/ │ │ ├── api/ │ │ │ ├── ModEntry.java │ │ │ ├── PhysicalSide.java │ │ │ ├── annotation/ │ │ │ │ ├── InternalUse.java │ │ │ │ └── OnlyFor.java │ │ │ ├── block/ │ │ │ │ └── IBlockHooks.java │ │ │ ├── commands/ │ │ │ │ ├── IEnumCommand.java │ │ │ │ ├── PermissionLevel.java │ │ │ │ └── args/ │ │ │ │ ├── ArgumentSerializer.java │ │ │ │ ├── FontArgument.java │ │ │ │ ├── SingletonArgumentInfo.java │ │ │ │ └── TagArgument.java │ │ │ ├── data/ │ │ │ │ ├── BookshelfTags.java │ │ │ │ ├── ISidedRecipeManager.java │ │ │ │ ├── codecs/ │ │ │ │ │ ├── EnumStreamCodec.java │ │ │ │ │ ├── map/ │ │ │ │ │ │ ├── MapCodecHelper.java │ │ │ │ │ │ ├── MapCodecs.java │ │ │ │ │ │ └── RegistryMapCodecHelper.java │ │ │ │ │ └── stream/ │ │ │ │ │ └── StreamCodecs.java │ │ │ │ ├── conditions/ │ │ │ │ │ ├── ConditionType.java │ │ │ │ │ ├── ILoadCondition.java │ │ │ │ │ └── LoadConditions.java │ │ │ │ ├── enchantment/ │ │ │ │ │ └── EnchantmentLevel.java │ │ │ │ ├── ingredient/ │ │ │ │ │ └── IngredientLogic.java │ │ │ │ └── loot/ │ │ │ │ ├── PoolTarget.java │ │ │ │ └── modifiers/ │ │ │ │ └── LootPoolAddition.java │ │ │ ├── entity/ │ │ │ │ └── villager/ │ │ │ │ ├── MerchantTier.java │ │ │ │ └── trades/ │ │ │ │ ├── VillagerBuys.java │ │ │ │ ├── VillagerOffers.java │ │ │ │ └── VillagerSells.java │ │ │ ├── function/ │ │ │ │ ├── CachedSupplier.java │ │ │ │ ├── QuadConsumer.java │ │ │ │ ├── ReloadableCache.java │ │ │ │ ├── SidedReloadableCache.java │ │ │ │ ├── TriConsumer.java │ │ │ │ └── TriFunction.java │ │ │ ├── item/ │ │ │ │ └── IItemHooks.java │ │ │ ├── loot/ │ │ │ │ ├── LootPoolEntryDescriber.java │ │ │ │ └── LootPoolEntryDescriptions.java │ │ │ ├── menu/ │ │ │ │ ├── data/ │ │ │ │ │ └── BlockPosData.java │ │ │ │ └── slot/ │ │ │ │ ├── InputSlot.java │ │ │ │ └── OutputSlot.java │ │ │ ├── network/ │ │ │ │ ├── AbstractPacket.java │ │ │ │ ├── Destination.java │ │ │ │ ├── INetworkHandler.java │ │ │ │ └── IPacket.java │ │ │ ├── registry/ │ │ │ │ ├── ContentProvider.java │ │ │ │ ├── RegistrationContext.java │ │ │ │ ├── RegistryReference.java │ │ │ │ └── adapters/ │ │ │ │ ├── GameRegistryAdapter.java │ │ │ │ ├── GenericRegistryAdapter.java │ │ │ │ └── RegistryAdapter.java │ │ │ ├── service/ │ │ │ │ └── Services.java │ │ │ ├── text/ │ │ │ │ ├── font/ │ │ │ │ │ ├── BuiltinFonts.java │ │ │ │ │ └── IFontEntry.java │ │ │ │ ├── format/ │ │ │ │ │ ├── IPropertyFormat.java │ │ │ │ │ └── PropertyFormat.java │ │ │ │ └── unit/ │ │ │ │ ├── IUnit.java │ │ │ │ └── Units.java │ │ │ └── util/ │ │ │ ├── CommandHelper.java │ │ │ ├── DataHelper.java │ │ │ ├── ExperienceHelper.java │ │ │ ├── FunctionHelper.java │ │ │ ├── IGameplayHelper.java │ │ │ ├── IPlatformHelper.java │ │ │ ├── IRenderHelper.java │ │ │ ├── MathsHelper.java │ │ │ ├── TextHelper.java │ │ │ └── TickAccumulator.java │ │ ├── impl/ │ │ │ ├── BookshelfContent.java │ │ │ ├── BookshelfMod.java │ │ │ ├── Constants.java │ │ │ ├── DebugContentProvider.java │ │ │ ├── command/ │ │ │ │ ├── BlockTagToItemTagCommand.java │ │ │ │ ├── DebugCommands.java │ │ │ │ ├── EnchantCommand.java │ │ │ │ ├── FontCommand.java │ │ │ │ ├── HandCommand.java │ │ │ │ ├── RenameCommand.java │ │ │ │ ├── StructureCommand.java │ │ │ │ └── TranslateCommand.java │ │ │ ├── data/ │ │ │ │ ├── conditions/ │ │ │ │ │ ├── And.java │ │ │ │ │ ├── ModLoaded.java │ │ │ │ │ ├── Not.java │ │ │ │ │ ├── OnPlatform.java │ │ │ │ │ ├── Or.java │ │ │ │ │ └── RegistryContains.java │ │ │ │ ├── criterion/ │ │ │ │ │ ├── item/ │ │ │ │ │ │ └── NamespaceItemPredicate.java │ │ │ │ │ └── trigger/ │ │ │ │ │ └── AdvancementTrigger.java │ │ │ │ ├── ingredient/ │ │ │ │ │ ├── AllOfIngredient.java │ │ │ │ │ ├── BlockTagIngredient.java │ │ │ │ │ ├── EitherIngredient.java │ │ │ │ │ ├── FalseIngredient.java │ │ │ │ │ └── ModIdIngredient.java │ │ │ │ └── loot/ │ │ │ │ ├── entries/ │ │ │ │ │ └── LootItemStack.java │ │ │ │ └── modifiers/ │ │ │ │ ├── FingerprintCodec.java │ │ │ │ ├── ILootPoolHooks.java │ │ │ │ └── LootModificationHandler.java │ │ │ ├── recipe/ │ │ │ │ └── RecipeTypeImpl.java │ │ │ ├── registry/ │ │ │ │ └── adapter/ │ │ │ │ ├── BlockEntityRendererAdapter.java │ │ │ │ ├── BlockRegistryAdapter.java │ │ │ │ ├── BlockRenderTypeAdapter.java │ │ │ │ ├── CommandArgumentAdapter.java │ │ │ │ ├── CreativeModeTabAdapter.java │ │ │ │ ├── IngredientTypeAdapter.java │ │ │ │ ├── LootDescriptionAdapter.java │ │ │ │ ├── LootEntryTypeAdapter.java │ │ │ │ ├── LootPoolAdditionAdapter.java │ │ │ │ ├── MenuScreenAdapter.java │ │ │ │ ├── MenuTypeAdapter.java │ │ │ │ ├── PacketAdapter.java │ │ │ │ ├── PotPatternAdapter.java │ │ │ │ ├── PotionBrewAdapter.java │ │ │ │ ├── RecipeTypeAdapter.java │ │ │ │ ├── SoundEventAdapter.java │ │ │ │ └── VillagerTradeAdapter.java │ │ │ └── resources/ │ │ │ └── ExtendedText.java │ │ └── mixin/ │ │ ├── access/ │ │ │ ├── block/ │ │ │ │ ├── AccessorBannerBlockEntity.java │ │ │ │ ├── AccessorBaseContainerBlockEntity.java │ │ │ │ ├── AccessorBlockEntityRenderers.java │ │ │ │ └── AccessorCropBlock.java │ │ │ ├── client/ │ │ │ │ ├── AccessorFontManager.java │ │ │ │ ├── AccessorItemBlockRenderTypes.java │ │ │ │ ├── AccessorMinecraft.java │ │ │ │ └── gui/ │ │ │ │ └── AccessorAbstractWidget.java │ │ │ ├── entity/ │ │ │ │ └── AccessorEntity.java │ │ │ ├── level/ │ │ │ │ └── AccessorRecipeManager.java │ │ │ ├── loot/ │ │ │ │ ├── AccessorCompositeEntryBase.java │ │ │ │ ├── AccessorDynamicLoot.java │ │ │ │ ├── AccessorLootItem.java │ │ │ │ ├── AccessorLootPool.java │ │ │ │ ├── AccessorLootPoolSingletonContainer.java │ │ │ │ ├── AccessorLootTable.java │ │ │ │ ├── AccessorNestedLootTable.java │ │ │ │ └── AccessorTagEntry.java │ │ │ └── particles/ │ │ │ └── AccessSimpleParticleType.java │ │ └── patch/ │ │ ├── advancement/ │ │ │ └── MixinPlayerAdvancements.java │ │ ├── block/ │ │ │ └── MixinDecoratedPotPatterns.java │ │ ├── client/ │ │ │ └── MixinClientPacketListener.java │ │ ├── entity/ │ │ │ ├── MixinLightningBolt.java │ │ │ └── MixinLivingEntity.java │ │ ├── item/ │ │ │ └── MixinCreativeModeTab.java │ │ ├── level/ │ │ │ ├── MixinRecipeManager.java │ │ │ └── MixinWalkNodeEvaluator.java │ │ ├── locale/ │ │ │ └── MixinClientLanguage.java │ │ ├── loot/ │ │ │ ├── MixinLootDataType.java │ │ │ ├── MixinLootItemKilledByPlayerCondition.java │ │ │ └── MixinLootPool.java │ │ ├── packs/ │ │ │ └── MixinSimpleJsonResourceReloadListener.java │ │ ├── potions/ │ │ │ └── MixinPotionBrewing.java │ │ └── server/ │ │ └── MixinReloadableServerResources.java │ └── resources/ │ ├── META-INF/ │ │ └── services/ │ │ └── net.darkhax.bookshelf.common.api.registry.ContentProvider │ ├── assets/ │ │ └── bookshelf/ │ │ └── lang/ │ │ ├── en_us.json │ │ ├── es_ar.json │ │ ├── ja_jp.json │ │ ├── pt_br.json │ │ └── zh_cn.json │ ├── bookshelf.mixins.json │ ├── data/ │ │ └── bookshelf/ │ │ ├── damage_type/ │ │ │ └── fake_player.json │ │ └── tags/ │ │ ├── damage_type/ │ │ │ └── fake_player.json │ │ └── item/ │ │ └── creative_tab/ │ │ └── minecraft/ │ │ ├── building_blocks.json │ │ ├── colored_blocks.json │ │ ├── combat.json │ │ ├── food_and_drinks.json │ │ ├── functional_blocks.json │ │ ├── ingredients.json │ │ ├── natural_blocks.json │ │ ├── op_blocks.json │ │ ├── redstone_blocks.json │ │ ├── spawn_eggs.json │ │ └── tools_and_utilities.json │ └── pack.mcmeta ├── fabric/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── net/ │ │ └── darkhax/ │ │ └── bookshelf/ │ │ └── fabric/ │ │ └── impl/ │ │ ├── FabricMod.java │ │ ├── FabricModClient.java │ │ ├── data/ │ │ │ └── FabricIngredient.java │ │ ├── network/ │ │ │ └── FabricNetworkHandler.java │ │ └── util/ │ │ ├── FabricGameplayHelper.java │ │ ├── FabricPlatformHelper.java │ │ ├── FabricRegistryHelper.java │ │ └── FabricRenderHelper.java │ └── resources/ │ ├── META-INF/ │ │ └── services/ │ │ ├── net.darkhax.bookshelf.common.api.network.INetworkHandler │ │ ├── net.darkhax.bookshelf.common.api.util.IGameplayHelper │ │ ├── net.darkhax.bookshelf.common.api.util.IPlatformHelper │ │ └── net.darkhax.bookshelf.common.api.util.IRenderHelper │ ├── bookshelf.fabric.mixins.json │ └── fabric.mod.json ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── neoforge/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── net/ │ │ └── darkhax/ │ │ └── bookshelf/ │ │ └── neoforge/ │ │ ├── impl/ │ │ │ ├── NeoForgeMod.java │ │ │ ├── data/ │ │ │ │ └── NeoForgeIngredient.java │ │ │ ├── network/ │ │ │ │ └── NeoForgeNetworkHandler.java │ │ │ └── util/ │ │ │ ├── NeoForgeGameplayHelper.java │ │ │ ├── NeoForgePlatformHelper.java │ │ │ ├── NeoForgeRegistryHelper.java │ │ │ └── NeoForgeRenderHelper.java │ │ └── mixin/ │ │ └── access/ │ │ └── gui/ │ │ └── screen/ │ │ └── AccessorMenuScreens.java │ └── resources/ │ ├── META-INF/ │ │ ├── neoforge.mods.toml │ │ └── services/ │ │ ├── net.darkhax.bookshelf.common.api.network.INetworkHandler │ │ ├── net.darkhax.bookshelf.common.api.util.IGameplayHelper │ │ ├── net.darkhax.bookshelf.common.api.util.IPlatformHelper │ │ └── net.darkhax.bookshelf.common.api.util.IRenderHelper │ └── bookshelf.neoforge.mixins.json └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text eol=lf *.bat text eol=crlf *.patch text eol=lf *.java text eol=lf *.gradle text eol=crlf *.png binary *.gif binary *.exe binary *.dll binary *.jar binary *.lzma binary *.zip binary *.pyd binary *.cfg text eol=lf *.jks binary ================================================ FILE: .gitignore ================================================ # eclipse bin *.launch .settings .metadata .classpath .project # idea out *.ipr *.iws *.iml .idea/* !.idea/scopes # gradle build .gradle # other eclipse run runs .profileconfig.json ================================================ FILE: Jenkinsfile ================================================ #!/usr/bin/env groovy pipeline { agent any tools { jdk "jdk-21" } stages { stage('Setup') { steps { echo 'Setup Project' sh 'chmod +x gradlew' sh './gradlew clean' } } stage('Build') { steps { withCredentials([ file(credentialsId: 'build_secrets', variable: 'ORG_GRADLE_PROJECT_secretFile'), file(credentialsId: 'java_keystore', variable: 'ORG_GRADLE_PROJECT_keyStore'), file(credentialsId: 'gpg_key', variable: 'ORG_GRADLE_PROJECT_pgpKeyRing') ]) { echo 'Building project.' sh './gradlew build publish publishCurseForge modrinth updateVersionTracker --stacktrace --warn' } } } } } ================================================ FILE: LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. 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 not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the 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 specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY ( INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms ( or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: README.md ================================================ # Bookshelf [![CurseForge Project](https://img.shields.io/curseforge/dt/228525?logo=curseforge&label=CurseForge&style=flat-square&labelColor=2D2D2D&color=555555)](https://www.curseforge.com/minecraft/mc-mods/bookshelf) [![Modrinth Project](https://img.shields.io/modrinth/dt/uy4Cnpcm?logo=modrinth&label=Modrinth&style=flat-square&labelColor=2D2D2D&color=555555)](https://modrinth.com/mod/bookshelf-lib) [![Maven Project](https://img.shields.io/maven-metadata/v?style=flat-square&logoColor=D31A38&labelColor=2D2D2D&color=555555&label=Latest&logo=gradle&metadataUrl=https%3A%2F%2Fmaven.blamejared.com%2Fnet%2Fdarkhax%2Fbookshelf%2Fbookshelf-common-1.21.1%2Fmaven-metadata.xml)](https://maven.blamejared.com/net/darkhax/bookshelf) Bookshelf is a library mod that provides code, frameworks, and utilities for other mods. Many mods make use of Bookshelf and are powered by its code. The documentation for this mod can be found [here](https://docs.darkhax.net/mods/bookshelf). ## Why use a library mod? Library mods like Bookshelf allow seemingly unrelated mods to reuse parts of the same code base. This reduces the amount of time required to develop and maintain certain mods and features. Library code is also tested in a wider range of circumstances and communities which can lead to fewer bugs and faster code. ## Built With Bookshelf The following mods were built using Bookshelf and are powered by its code! - [Enchantment Descriptions](https://www.curseforge.com/minecraft/mc-mods/enchantment-descriptions) - Adds in-game descriptions for enchantments to tooltips. - [Botany Pots](https://www.curseforge.com/minecraft/mc-mods/botany-pots) - Adds pots that you can use to grow crops! - [Tips](https://www.curseforge.com/minecraft/mc-mods/tips) - Adds tips to various loading screens. - [Dark Utilities](https://www.curseforge.com/minecraft/mc-mods/dark-utilities) - Blocks and items with interesting effects and abilities. ## Maven Dependency If you are using [Gradle](https://gradle.org) to manage your dependencies, add the following into your `build.gradle` file. Make sure to replace the version with the correct one. All versions can be viewed [here](https://maven.blamejared.com/net/darkhax/bookshelf). ```gradle repositories { maven { url 'https://maven.blamejared.com' } } dependencies { // NeoForge implementation group: 'net.darkhax.bookshelf', name: 'bookshelf-neoforge-1.21.1', version: '21.1.0' // Forge implementation group: 'net.darkhax.bookshelf', name: 'bookshelf-forge-1.21.1', version: '21.1.0' // Fabric & Quilt modImplementation group: 'net.darkhax.bookshelf', name: 'bookshelf-fabric-1.21.1', version: '21.1.0' // Common / MultiLoader / Vanilla compileOnly group: 'net.darkhax.bookshelf', name: 'bookshelf-common-1.21.1', version: '21.1.0' } ``` ## Sponsors [![](https://assets.blamejared.com/nodecraft/darkhax.jpg)](https://nodecraft.com/r/darkhax) Bookshelf is sponsored by Nodecraft. Use code **[DARKHAX](https://nodecraft.com/r/darkhax)** for 30% of your first month of service! ================================================ FILE: build.gradle ================================================ plugins { id 'secret-loader' id 'fabric-loom' version '1.11-SNAPSHOT' apply false id 'net.neoforged.moddev' version '2.0.112' apply false id 'net.darkhax.curseforgegradle' version '1.1.25' apply(false) id 'com.modrinth.minotaur' version '2.8.7' apply(false) id 'project_validation' id 'build-number' id 'git-changelog' id 'patreon' id 'version-checker' id 'readme-update' } ================================================ FILE: buildSrc/build.gradle ================================================ plugins { id 'groovy-gradle-plugin' } repositories { mavenCentral() } dependencies { implementation group: 'com.diluv.schoomp', name: 'Schoomp', version: '1.2.6' } ================================================ FILE: buildSrc/src/main/groovy/build-number.gradle ================================================ // This script attempts to append the build number to the project version. // The build number is read from an environment variable that is set by // a CI like Jenkins. If the build number is missing it will default to 0. var buildNumber = System.getenv('BUILD_NUMBER') ? System.getenv('BUILD_NUMBER') : 0 project.version = "${project.version}.${buildNumber}".toString() project.getSubprojects().each { proj -> proj.version = project.version } project.logger.lifecycle("Appending build number to version. Build #${buildNumber} Version is now ${project.version}") ================================================ FILE: buildSrc/src/main/groovy/git-changelog.gradle ================================================ project.ext.mod_changelog = 'No changelog was provided. Please refer to the project page for more information.' try { project.mod_changelog = 'No changelog was provided. Please refer to the project page for more information.' def gitCommit = System.getenv('GIT_COMMIT') ?: getExecOutput(['git', 'log', '-n', '1', '--pretty=tformat:%h']) def gitPrevCommit = System.getenv('GIT_PREVIOUS_COMMIT') // If a full range is available use that range. if (gitCommit && gitPrevCommit) { project.ext.mod_changelog = getExecOutput(['git', 'log', "--pretty=tformat:- %s", '' + gitPrevCommit + '..' + gitCommit]) project.logger.lifecycle("Generated changelog using commits ${gitPrevCommit} to ${gitCommit}.") } // If only one commit is available, use the last commit. else if (gitCommit) { project.ext.mod_changelog = getExecOutput(['git', 'log', '' + "--pretty=tformat:- %s", '-1', '' + gitCommit]) project.logger.lifecycle("Generated changelog using commit ${gitCommit}.") } } catch (Exception e) { project.logger.warn("Changelogs could not be generated! ${e.message}") } def getExecOutput(commands) { def out = new ByteArrayOutputStream() exec { commandLine commands standardOutput out } return out.toString().trim() } ================================================ FILE: buildSrc/src/main/groovy/minify-json.gradle ================================================ // This script will minify JSON files as the project is built. This is // done by removing unused whitespace and newlines from the file. The // original source file is not modified. // // Minifying JSON files will produce a smaller JAR file. This will make // upload/download times faster, reduce bandwidth usage, and reduce the // amount of storage space required to use or host the project. // // Minified JSON files are also faster to read and parse. The reduced file // size makes them faster to stream from disk, and the JSON tokenizer does // not need to waste cycles handling unnecessary data. import groovy.json.JsonOutput import groovy.json.JsonSlurper processResources { doLast { def jsonMinifyStart = System.currentTimeMillis() def jsonMinified = 0 def jsonBytesSaved = 0 fileTree(dir: outputs.files.asPath, include: ['**/*.json', '**/*.mcmeta']).each { try { def oldLength = it.length() it.text = JsonOutput.toJson(new JsonSlurper().parse(it)) jsonBytesSaved += oldLength - it.length() jsonMinified++ } catch (Exception e) { project.logger.error("Failed to minify file '${it.path}'.") throw e } } project.logger.lifecycle("Minified ${jsonMinified} files. Saved ${jsonBytesSaved} bytes before compression. Took ${System.currentTimeMillis() - jsonMinifyStart}ms.") } } ================================================ FILE: buildSrc/src/main/groovy/multiloader-common.gradle ================================================ plugins { id 'java-library' id 'maven-publish' id 'minify-json' } base { archivesName = "${mod_id}-${project.name}-${minecraft_version}" } java { toolchain.languageVersion = JavaLanguageVersion.of(java_version) withSourcesJar() withJavadocJar() } javadoc { options.addStringOption('Xdoclint:-missing', '-quiet') } repositories { mavenCentral() exclusiveContent { forRepository { maven { name = 'Sponge' url = 'https://repo.spongepowered.org/repository/maven-public' } } filter { includeGroupAndSubgroups('org.spongepowered') } } exclusiveContent { forRepositories( maven { name = 'ParchmentMC' url = 'https://maven.parchmentmc.org/' }, maven { name = "NeoForge" url = 'https://maven.neoforged.net/releases' } ) filter { includeGroup('org.parchmentmc.data') } } exclusiveContent { forRepository { maven { url "https://cursemaven.com" } } filter { includeGroup "curse.maven" } } maven { name = 'BlameJared' url = 'https://maven.blamejared.com' } } ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> configurations."$variant".outgoing { capability("$group:${base.archivesName.get()}:$version") capability("$group:$mod_id-${project.name}-${minecraft_version}:$version") capability("$group:$mod_id:$version") } publishing.publications.configureEach { suppressPomMetadataWarningsFor(variant) } } sourcesJar { from(rootProject.file('LICENSE')) { rename { "license_${mod_id}.txt" } } } jar { from(rootProject.file('LICENSE')) { rename { "license_${mod_id}.txt" } } manifest { attributes([ 'Specification-Title' : mod_name, 'Specification-Vendor' : mod_author, 'Specification-Version' : project.jar.archiveVersion, 'Implementation-Title' : project.name, 'Implementation-Version': project.jar.archiveVersion, 'Implementation-Vendor' : mod_author, 'Built-On-Minecraft' : minecraft_version, 'CurseForge' : curse_page, 'Modrinth' : modrinth_page ]) } } processResources { var expandProps = [ 'version' : project.version, 'group' : project.group, 'platform' : project.name, 'minecraft_version' : minecraft_version, 'minecraft_version_range' : minecraft_version_range, 'mod_name' : mod_name, 'mod_author' : mod_author, 'mod_id' : mod_id, 'mod_repo' : mod_repo, 'mod_license' : mod_license, 'mod_description' : mod_description, 'mod_item_icon' : mod_item_icon, 'neoforge_version' : neoforge_version, 'neoforge_loader_version_range': neoforge_loader_version_range, 'fabric_version' : fabric_version, 'fabric_loader_version' : fabric_loader_version, 'java_version' : java_version, 'curse_project' : curse_project, 'curse_page' : curse_page, 'modrinth_project' : modrinth_project, 'modrinth_page' : modrinth_page, 'mod_client_only' : mod_client_only, 'patreon_pledges' : rootProject.ext.patreon.pledgeNames, 'patreon_url' : rootProject.ext.patreon.campaignUrl ] boolean clientOnly = project.hasProperty('mod_client_only') && project.findProperty('mod_client_only') == 'true' if ('fabric' == project.name) { expandProps.put('mod_target_environment', clientOnly ? 'client' : '*') expandProps.put('mod_target_environment', clientOnly ? 'client' : '*') } filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'META-INF/neoforge.mods.toml', '*.mixins.json']) { expand expandProps } inputs.properties(expandProps) } publishing { publications { register('mavenJava', MavenPublication) { artifactId base.archivesName.get() from components.java } } repositories { maven { url System.getenv('local_maven_url') } } } ================================================ FILE: buildSrc/src/main/groovy/multiloader-loader.gradle ================================================ plugins { id 'multiloader-common' } configurations { commonJava{ canBeResolved = true } commonResources{ canBeResolved = true } } dependencies { compileOnly(project(':common')) { capabilities { requireCapability "$group:$mod_id" } } commonJava project(path: ':common', configuration: 'commonJava') commonResources project(path: ':common', configuration: 'commonResources') } tasks.named('compileJava', JavaCompile) { dependsOn(configurations.commonJava) source(configurations.commonJava) } processResources { dependsOn(configurations.commonResources) from(configurations.commonResources) } tasks.named('javadoc', Javadoc).configure { dependsOn(configurations.commonJava) source(configurations.commonJava) } tasks.named('sourcesJar', Jar) { dependsOn(configurations.commonJava) from(configurations.commonJava) dependsOn(configurations.commonResources) from(configurations.commonResources) } ================================================ FILE: buildSrc/src/main/groovy/patreon.gradle ================================================ import groovy.json.JsonSlurper project.ext.patreon = [ pledgeNames : '', campaignUrl : '' ] project.ext.mod_supporters = 'No supporters loaded.' if (project.hasProperty('patreon_campaign_id') && project.hasProperty('patreon_auth_token')) { project.ext.patreon.campaign = project.findProperty('patreon_campaign_id') def authToken = project.findProperty('patreon_auth_token') getPledges(project.ext.patreon.campaign, authToken) if (project.hasProperty('patreon_campaign_url')) { project.ext.patreon.campaignUrl = project.getProperty('patreon_campaign_url') } project.logger.lifecycle("Loading pledge data for default campaign ${project.ext.patreon.campaign}.") } else { project.logger.warn("Patreon data can not be loaded! has_id:${project.hasProperty('patreon_campaign_id')} has_campaign:${project.hasProperty('patreon_auth_token')}") } /* Gets a list of pledges for a specified campaign using a specified auth token. */ def getPledges(campaignId, authToken) { // Connect to the Patreon API using the provided auth info. def connection = new URL('https://www.patreon.com/api/oauth2/api/campaigns/' + campaignId + '/pledges').openConnection() as HttpURLConnection connection.setRequestProperty('User-Agent', 'Patreon-Groovy, platform ' + System.properties['os.name'] + ' ' + System.properties['os.version']) connection.setRequestProperty('Authorization', 'Bearer ' + authToken) connection.setRequestProperty('Accept', 'application/json') // Check if connection was valid. if (connection.responseCode == 200) { // Map holding pledge data. The key is a string representation of the // users ID and the value is an object holding information about the // pledge. Map pledges = new HashMap() // Parse the response into an ambiguous json object. def json = connection.inputStream.withCloseable { inStream -> new JsonSlurper().parse(inStream as InputStream) } // Iterate all the pledge entries for (pledgeInfo in json.data) { // Create new pledge entry, and set pledge specific info. def pledge = new Pledge() pledge.id = pledgeInfo.relationships.patron.data.id pledge.amountInCents = pledgeInfo.attributes.amount_cents pledge.declined = pledgeInfo.attributes.declined_since if (pledge.isValid()) { pledges.put(pledge.id, pledge) } } // Parse out the pledges display info from the JSON. for (pledgeInfo in json.included) { // Get pledge by user ID def pledge = pledges.get(pledgeInfo.id) // If the pledge exists, set the user data. if (pledge != null) { def info = pledgeInfo.attributes pledge.name = info.full_name pledge.vanityName = info.vanity } } def pledgeNames = new ArrayList<>() def pledgeLog = '' List validPledges = new ArrayList<>() for (entry in pledges) { def currentPledge = entry.value validPledges.add(currentPledge) pledgeLog += "- ${currentPledge.getDisplayName()}\n" pledgeNames.add(currentPledge.getDisplayName()) } project.ext.patreon.pledges = validPledges project.ext.patreon.pledgeNames = pledgeNames.join(', ') project.ext.patreon.pledgeLog = pledgeLog } } class Pledge { // The ID for this user in the Patreon system. def id // The amount this user is currently paying in USD cents. def amountInCents // The date they declined. This will be null if they haven't declined. def declined // The full name of the user. def name // The vanity name of the user, like a display name. def vanityName /* Checks if the user is valid, and is paying. */ def isValid() { return declined == null && amountInCents > 0 } /* Gets the display name for the user. Defaults to full name if no vanity name is specified by the user. */ def getDisplayName() { return vanityName != null ? vanityName : name } } ================================================ FILE: buildSrc/src/main/groovy/project_validation.gradle ================================================ import javax.imageio.ImageIO import java.awt.image.BufferedImage gradle.taskGraph.whenReady { graph -> // Validate the logo file for the project. // - logo.png must exist in the root project folder. // - logo.png must be a 1:1 aspect ratio. final File logoFile = project('common').file("src/main/resources/logo_${mod_id}.png") if (!logoFile.exists()) { throw new GradleException("A logo_${mod_id}.png file is required to build this mod.") } else { try { final BufferedImage logoImage = ImageIO.read(logoFile) if (logoImage.getWidth() != logoImage.getHeight()) { throw new GradleException('The logo image must be a 1:1 aspect ratio.') } } catch (IOException e) { throw new GradleException('Unable to process logo file.', e) } } // Validate the license file for the project. // - A file named LICENSE must exist in the root project folder. if (!rootProject.file('LICENSE').exists()) { throw new GradleException('LICENSE file does not exist.') } } ================================================ FILE: buildSrc/src/main/groovy/readme-update.gradle ================================================ task updateReadme { var readme = rootProject.file('README.md') if (!readme.exists()) { throw new GradleException('The README.md file is missing!') } doLast { rootProject.logger.lifecycle('Updating the README.md file...') var text = readme.text text = updateSection(text, 'name', buildName(rootProject)) text = updateSection(text, 'description', buildIntro(rootProject)) text = updateSection(text, 'maven', buildMavenInfo(rootProject)) text = updateSection(text, 'sponsor', buildSponsors(rootProject)) readme.text = text } } static String buildName(Project project) { var encodedPath = project.group.replaceAll('\\.', '%2F') var projectPath = project.group.replaceAll('\\.', '/') var modId = project.property('mod_id') var mcVersion = project.property('minecraft_version') var curseBadge = "[![CurseForge Project](https://img.shields.io/curseforge/dt/${project.property('curse_project')}?logo=curseforge&label=CurseForge&style=flat-square&labelColor=2D2D2D&color=555555)](${project.property('curse_page')})" var modrinthBadge = "[![Modrinth Project](https://img.shields.io/modrinth/dt/${project.property('modrinth_project')}?logo=modrinth&label=Modrinth&style=flat-square&labelColor=2D2D2D&color=555555)](${project.property('modrinth_page')})" var versionBadge = "[![Maven Project](https://img.shields.io/maven-metadata/v?style=flat-square&logoColor=D31A38&labelColor=2D2D2D&color=555555&label=Latest&logo=gradle&metadataUrl=https%3A%2F%2Fmaven.blamejared.com%2F${encodedPath}%2F${modId}-common-${mcVersion}%2Fmaven-metadata.xml)](https://maven.blamejared.com/${projectPath})" return "# ${project.property('mod_name')} ${curseBadge} ${modrinthBadge} ${versionBadge}" } static String buildIntro(Project project) { return "${project.property('mod_description')} The documentation for this mod can be found [here](${project.property('mod_docs')})." } static String buildMavenInfo(Project project) { var group = project.property('group') var modId = project.property('mod_id') var mcVersion = project.property('minecraft_version') var projectVersion = project.version return """|## Maven Dependency | |If you are using [Gradle](https://gradle.org) to manage your dependencies, add the following into your `build.gradle` file. Make sure to replace the version with the correct one. All versions can be viewed [here](https://maven.blamejared.com/${group.replaceAll('\\.', '/')}). | |```gradle |repositories { | maven { | url 'https://maven.blamejared.com' | } |} | |dependencies { | // NeoForge | implementation group: '${group}', name: '${modId}-neoforge-${mcVersion}', version: '${projectVersion}' | | // Forge | implementation group: '${group}', name: '${modId}-forge-${mcVersion}', version: '${projectVersion}' | | // Fabric & Quilt | modImplementation group: '${group}', name: '${modId}-fabric-${mcVersion}', version: '${projectVersion}' | | // Common / MultiLoader / Vanilla | compileOnly group: '${group}', name: '${modId}-common-${mcVersion}', version: '${projectVersion}' |} |```""".stripMargin() } static String buildSponsors(Project project) { var modName = project.property('mod_name') return """|## Sponsors | |[![](https://assets.blamejared.com/nodecraft/darkhax.jpg)](https://nodecraft.com/r/darkhax) |${modName} is sponsored by Nodecraft. Use code **[DARKHAX](https://nodecraft.com/r/darkhax)** for 30% of your first month of service!""".stripMargin() } static String updateSection(String inputText, String region, String text) { var startComment = commentOf("${region}-start") var startPos = inputText.indexOf(startComment) + startComment.length() var endComment = commentOf("${region}-end") var endPos = inputText.indexOf(endComment) return inputText.substring(0, startPos) + System.lineSeparator() + text + System.lineSeparator() + inputText.substring(endPos) } static String commentOf(String commentText) { return "" } ================================================ FILE: buildSrc/src/main/groovy/secret-loader.gradle ================================================ // Loads properties from a file containing environmental secrets. import groovy.json.JsonSlurper // Auto detects a secret file and injects it. if (rootProject.hasProperty('secretFile')) { project.logger.lifecycle('Automatically loading properties from the secretFile') final def secretsFile = rootProject.file(rootProject.findProperty('secretFile')) if (secretsFile.exists() && secretsFile.name.endsWith('.json')) { loadProperties(secretsFile) } else { project.logger.lifecycle("Properties could not be read from the secretFile because it does not exist. ${secretsFile}") } } else { project.logger.lifecycle('The secretFile property has not been set. Some API tokens will not be available.') } // Loads properties using a specified json file. def loadProperties(propertyFile) { if (propertyFile.exists()) { propertyFile.withReader { Map propMap = new JsonSlurper().parse it for (entry in propMap) { // Filter entries that use _comment in the key. if (!entry.key.endsWith('_comment')) { project.ext.set(entry.key, entry.value) } } project.logger.lifecycle("Successfully loaded ${propMap.size()} environment secrets.") propMap.clear() } } else { project.logger.warn("Could not find property file! Expected: ${propertyFile}") } } ================================================ FILE: buildSrc/src/main/groovy/version-checker.gradle ================================================ // This plugin adds a task that can update the latest version on Jared's // update checker API. This is a private service that requires an API key // to use. For more information contact Jared https://x.com/jaredlll08 import groovy.json.JsonOutput task updateVersionTracker { if (!rootProject.hasProperty('versionTrackerAPI') || !rootProject.hasProperty('versionTrackerUsername')) { rootProject.logger.warn('Skipping Version Checker update. Authentication is required!') } onlyIf { rootProject.hasProperty('versionTrackerAPI') && rootProject.hasProperty('versionTrackerUsername') } doLast { def username = rootProject.findProperty('versionTrackerUsername') def apiKey = rootProject.findProperty('versionTrackerKey') // Creates a Map that acts as the Json body of the API request. def body = [ 'author' : username, 'projectName' : project.ext.mod_id, 'gameVersion' : project.ext.minecraft_version, 'projectVersion': project.version, 'homepage' : project.ext.curse_page, 'uid' : apiKey ] // Opens a connection to the version tracker API and writes the payload JSON. def req = new URL(rootProject.findProperty('versionTrackerAPI')).openConnection() req.setRequestMethod('POST') req.setRequestProperty('Content-Type', 'application/json; charset=UTF-8') req.setRequestProperty('User-Agent', "${project.ext.mod_name} Tracker Gradle") req.setDoOutput(true) req.getOutputStream().write(JsonOutput.toJson(body).getBytes("UTF-8")) // For the request to be sent we need to read data from the stream. project.logger.lifecycle("Version Check: Status ${req.getResponseCode()}") project.logger.lifecycle("Version Check: Response ${req.getInputStream().getText()}") } } ================================================ FILE: common/build.gradle ================================================ plugins { id 'multiloader-common' id 'net.neoforged.moddev' } neoForge { neoFormVersion = neo_form_version parchment { minecraftVersion = parchment_minecraft mappingsVersion = parchment_version } } dependencies { compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.4.0' annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.4.0' if (project.hasProperty('jei_version')) { compileOnly("mezz.jei:jei-${minecraft_version}-common-api:${jei_version}") } } configurations { commonJava { canBeResolved = false canBeConsumed = true } commonResources { canBeResolved = false canBeConsumed = true } } artifacts { commonJava sourceSets.main.java.sourceDirectories.singleFile commonResources sourceSets.main.resources.sourceDirectories.singleFile } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/ModEntry.java ================================================ package net.darkhax.bookshelf.common.api; public record ModEntry(String modId, String name, String description, String version) { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/PhysicalSide.java ================================================ package net.darkhax.bookshelf.common.api; /** * Represents a physical location in the client/server network diagram. */ public enum PhysicalSide { /** * A physical client. This includes single player, and LAN worlds. */ CLIENT, /** * A physical server. This includes dedicated servers where client code and logic is not accessible. */ SERVER; /** * Checks if this is a physical client. * * @return Returns true when on a physical client. */ public boolean isClient() { return this == CLIENT; } /** * Checks if this is a physical server. * * @return Returns true when on a physical server. */ public boolean isServer() { return this == SERVER; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/annotation/InternalUse.java ================================================ package net.darkhax.bookshelf.common.api.annotation; /** * A visual indicator for members that may be visible for technical reasons or convenience but are not intended for * general use. For example, if a method you did not create is decorated with this annotation you should not invoke it. */ public @interface InternalUse { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/annotation/OnlyFor.java ================================================ package net.darkhax.bookshelf.common.api.annotation; import net.darkhax.bookshelf.common.api.PhysicalSide; /** * A visual indicator that a class, field, or method can only be accessed in certain environments. There is no special * magic or ASM behind this annotation, it is only a visual indicator to help navigate the code. */ public @interface OnlyFor { PhysicalSide value(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/block/IBlockHooks.java ================================================ package net.darkhax.bookshelf.common.api.block; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.pathfinder.PathType; import org.jetbrains.annotations.Nullable; public interface IBlockHooks { Direction[] LIGHTNING_REDIRECTION_FACES = new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}; Direction[] NO_LIGHTNING_REDIRECTION_FACES = new Direction[]{}; /** * Allows the block to determine its own pathfinding type. * * @param state The current state of the block. * @param context Additional context from the world the block is in. * @param pos The position of the block. * @return The pathfinding type for the block. If null is returned the vanilla behavior for determining pathfinding * will be used instead. */ @Nullable default PathType getPathfindingType(BlockState state, BlockGetter context, BlockPos pos) { return null; } /** * Called when the block is directly struck by lightning. * * @param state The state of this block. * @param level The level. * @param pos The position of this block. * @param lightning The lightning bolt that hit the block. */ default void onLightningStrike(BlockState state, Level level, BlockPos pos, LightningBolt lightning) { } /** * Called when a neighbor is struck by lightning and the block is not insulated from the strike. * * @param state The state of this block. * @param level The level. * @param pos The position of this block. * @param lightning The lightning bolt that hit the block. * @param strikeOrigin The original strike position of the lightning bolt. */ default void onLightningStrikeIndirect(BlockState state, Level level, BlockPos pos, LightningBolt lightning, BlockPos strikeOrigin) { } /** * Provides an array of directions lightning can travel and indirectly hit when this block is hit by lightning. * * @param state The state of this block. * @param level The level. * @param pos The position of this block. * @return An array of directions that should be indirectly hit by the lightning. */ default Direction[] redirectLightningStrike(BlockState state, Level level, BlockPos pos) { return NO_LIGHTNING_REDIRECTION_FACES; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/IEnumCommand.java ================================================ package net.darkhax.bookshelf.common.api.commands; import com.mojang.brigadier.Command; import net.minecraft.commands.CommandSourceStack; /** * Allows an enum to be used as a branching command path. */ public interface IEnumCommand extends Command { /** * Gets the name of the command. This must be unique for each enum value. * * @return The name of the command. */ String getCommandName(); /** * Gets the required permission level to perform the command. * * @return The required permission level. */ default PermissionLevel requiredPermissionLevel() { return PermissionLevel.PLAYER; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/PermissionLevel.java ================================================ package net.darkhax.bookshelf.common.api.commands; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import java.util.function.Predicate; public enum PermissionLevel implements Predicate { /** * All players will generally meet the requirements for this permission level. */ PLAYER(Commands.LEVEL_ALL), /** * These players have slightly elevated permission levels. In vanilla, they do not gain access to any additional * commands, but they are able to bypass spawn chunk protection. */ MODERATOR(Commands.LEVEL_MODERATORS), /** * These players can execute commands that modify the world and player data. They are also allowed to use and modify * command blocks. */ GAMEMASTER(Commands.LEVEL_GAMEMASTERS), /** * These players can use commands related to player management. For example, they can ban, kick, op, and de-op. */ ADMIN(Commands.LEVEL_ADMINS), /** * This is the highest permission level available in vanilla Minecraft. Players with this permission level generally * have no restrictions. */ OWNER(Commands.LEVEL_OWNERS); final int level; PermissionLevel(int level) { this.level = level; } public int get() { return this.level; } @Override public boolean test(CommandSourceStack source) { return source.hasPermission(this.level); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/ArgumentSerializer.java ================================================ package net.darkhax.bookshelf.common.api.commands.args; import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.serialization.JsonOps; import com.mojang.serialization.MapCodec; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import org.jetbrains.annotations.NotNull; import java.util.function.BiFunction; import java.util.function.Function; public class ArgumentSerializer, V> implements ArgumentTypeInfo> { private final MapCodec codec; private final StreamCodec stream; private final BiFunction fromData; private final Function toData; public ArgumentSerializer(MapCodec codec, StreamCodec stream, BiFunction mapFunc, Function toData) { this.codec = codec; this.stream = stream; this.fromData = mapFunc; this.toData = toData; } @Override public void serializeToNetwork(ArgTemplate template, @NotNull FriendlyByteBuf buf) { this.stream.encode(buf, template.data); } @NotNull @Override public ArgTemplate deserializeFromNetwork(@NotNull FriendlyByteBuf buf) { return new ArgTemplate<>(this, this.stream.decode(buf)); } @Override public void serializeToJson(@NotNull ArgTemplate template, @NotNull JsonObject json) { json.add("value", this.codec.codec().encodeStart(JsonOps.INSTANCE, template.data).getOrThrow()); } @NotNull @Override public ArgTemplate unpack(@NotNull T t) { return new ArgTemplate<>(this, this.toData.apply(t)); } public static class ArgTemplate, V> implements ArgumentTypeInfo.Template { private final ArgumentSerializer type; private final V data; protected ArgTemplate(ArgumentSerializer type, V data) { this.type = type; this.data = data; } @NotNull @Override public T instantiate(@NotNull CommandBuildContext ctx) { return this.type.fromData.apply(ctx, this.data); } @NotNull @Override public ArgumentTypeInfo type() { return this.type; } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/FontArgument.java ================================================ package net.darkhax.bookshelf.common.api.commands.args; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.api.text.font.BuiltinFonts; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.resources.ResourceLocation; import java.util.Collection; import java.util.Set; import java.util.concurrent.CompletableFuture; public class FontArgument implements ArgumentType { public static final FontArgument ARGUMENT = new FontArgument(); public static final ArgumentTypeInfo SERIALIZER = SingletonArgumentInfo.of(() -> ARGUMENT); private static final Set EXAMPLES = Set.of(BuiltinFonts.DEFAULT.identifier().toString(), BuiltinFonts.ALT.identifier().toString(), BuiltinFonts.ILLAGER.identifier().toString()); public static ResourceLocation get(CommandContext context) { return get("font", context); } public static ResourceLocation get(String argName, CommandContext context) { return context.getArgument(argName, ResourceLocation.class); } public static RequiredArgumentBuilder argument() { return argument("font"); } public static RequiredArgumentBuilder argument(String argName) { return Commands.argument(argName, ARGUMENT); } @Override public ResourceLocation parse(StringReader reader) throws CommandSyntaxException { return ResourceLocation.read(reader); } @Override public Collection getExamples() { return EXAMPLES; } @Override public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { if (Services.PLATFORM.isPhysicalClient()) { return SharedSuggestionProvider.suggestResource(TextHelper.getRegisteredFonts(), builder); } return SharedSuggestionProvider.suggestResource(BuiltinFonts.FONT_IDS, builder); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/SingletonArgumentInfo.java ================================================ package net.darkhax.bookshelf.common.api.commands.args; import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.ArgumentType; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.network.FriendlyByteBuf; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * An argument info type that will always resolve to the same singleton instance. This is beneficial when the values are * already known by the client in advance, or may even be only known by the client. * * @param The argument type. */ public final class SingletonArgumentInfo> implements ArgumentTypeInfo> { /** * Creates argument info for a given argument instance. * * @param argSupplier A supplier that resolves the singleton instance. This supplier should always return the same * instance! * @param The argument type. * @return The argument info for the singleton. */ public static > SingletonArgumentInfo of(Supplier argSupplier) { return new SingletonArgumentInfo<>(argSupplier); } private final CachedSupplier> templateSupplier; private SingletonArgumentInfo(Supplier singletonSupplier) { this.templateSupplier = CachedSupplier.cache(() -> new Template<>(singletonSupplier, this)); } @Override public void serializeToNetwork(@NotNull Template tTemplate, @NotNull FriendlyByteBuf friendlyByteBuf) { // NO-OP } @NotNull @Override public Template deserializeFromNetwork(@NotNull FriendlyByteBuf buffer) { return this.templateSupplier.get(); } @Override public void serializeToJson(@NotNull Template tTemplate, @NotNull JsonObject jsonObject) { // NO-OP } @NotNull @Override public Template unpack(@NotNull T template) { return this.templateSupplier.get(); } /** * A template that holds a cached argument singleton. * * @param The argument type. */ public static class Template> implements ArgumentTypeInfo.Template { private final ArgumentTypeInfo info; private final Supplier singletonSupplier; protected Template(Supplier supplier, ArgumentTypeInfo info) { this.singletonSupplier = supplier; this.info = info; } @NotNull @Override public T instantiate(@NotNull CommandBuildContext ctx) { return this.singletonSupplier.get(); } @NotNull @Override public ArgumentTypeInfo type() { return this.info; } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/commands/args/TagArgument.java ================================================ package net.darkhax.bookshelf.common.api.commands.args; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.core.HolderLookup; import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.CompletableFuture; public class TagArgument implements ArgumentType> { private static final MapCodec>> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( ResourceLocation.CODEC.fieldOf("registry_name").forGetter(ResourceKey::location) ).apply(instance, ResourceKey::createRegistryKey)); private static final StreamCodec>> STREAM = StreamCodec.of( (buf, key) -> buf.writeResourceLocation(key.location()), buf -> ResourceKey.createRegistryKey(buf.readResourceLocation()) ); public static final ArgumentSerializer, ResourceKey>> SERIALIZER = new ArgumentSerializer<>(CODEC, STREAM, TagArgument::makeRaw, t -> t.registryKey); private static final Collection EXAMPLES = Arrays.asList("minecraft:dirt", "minecraft:axolotl_food", "minecraft:enchantable/bow"); private final HolderLookup registryLookup; private final ResourceKey> registryKey; @SuppressWarnings({"rawtypes", "unchecked"}) private static TagArgument makeRaw(CommandBuildContext context, ResourceKey registryKey) { return new TagArgument<>(context, registryKey); } private TagArgument(CommandBuildContext context, ResourceKey> registryKey) { this.registryKey = registryKey; this.registryLookup = context.lookupOrThrow(registryKey); } @Override public TagKey parse(StringReader reader) throws CommandSyntaxException { final ResourceLocation tagId = ResourceLocation.read(reader); return TagKey.create(this.registryKey, tagId); } @Override public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { return SharedSuggestionProvider.suggestResource(this.registryLookup.listTagIds().map(TagKey::location), builder); } @Override public Collection getExamples() { return EXAMPLES; } public static TagArgument arg(CommandBuildContext context, ResourceKey> registry) { return new TagArgument<>(context, registry); } @SuppressWarnings("unchecked") public static TagKey get(String argName, CommandContext context, ResourceKey> registry) { return (TagKey) context.getArgument(argName, TagKey.class); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/BookshelfTags.java ================================================ package net.darkhax.bookshelf.common.api.data; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import net.minecraft.world.damagesource.DamageType; public class BookshelfTags { public static TagKey FAKE_PLAYER_DAMAGE = TagKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "fake_player")); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/ISidedRecipeManager.java ================================================ package net.darkhax.bookshelf.common.api.data; public interface ISidedRecipeManager { void bookshelf$setLogicalClient(); void bookshelf$setLogicalServer(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/EnumStreamCodec.java ================================================ package net.darkhax.bookshelf.common.api.data.codecs; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import org.jetbrains.annotations.NotNull; public class EnumStreamCodec> implements StreamCodec { private final Class enumClass; public EnumStreamCodec(Class clazz) { this.enumClass = clazz; } @NotNull @Override public T decode(FriendlyByteBuf buf) { return buf.readEnum(enumClass); } @Override public void encode(FriendlyByteBuf buf, @NotNull T toWrite) { buf.writeEnum(toWrite); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/MapCodecHelper.java ================================================ package net.darkhax.bookshelf.common.api.data.codecs.map; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.util.random.SimpleWeightedRandomList; import net.minecraft.util.random.WeightedEntry; import java.lang.reflect.Array; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.IntFunction; /** * A CodecHelper wraps a Codec to provide a large amount of helpers and utilities to make working with the Codec easier * and more flexible. * * @param The type handled by the codec helper. */ public class MapCodecHelper { /** * The root codec that powers all the helpers and utilities offered by the CodecHelper. */ private final Codec elementCodec; /** * A function for creating an array of the codecs type. This allows us to create clean arrays for our codecs without * relying on sketchy code. */ private final IntFunction arrayBuilder; @SafeVarargs public MapCodecHelper(Codec elementCodec, T... vargs) { if (vargs.length > 0) { throw new IllegalArgumentException("The arrayBuilder must be empty!"); } this.elementCodec = elementCodec; this.arrayBuilder = size -> (T[]) Array.newInstance(vargs.getClass().getComponentType(), size); } /** * Gets a codec that can read and write single instances of the element. * * @return A Codec that can read and write single instances of the element. */ public Codec get() { return this.elementCodec; } /** * A helper for defining a field of this type in a RecordCodecBuilder. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodecBuilder. * @return A RecordCodecBuilder that represents a field. */ public RecordCodecBuilder get(String fieldName, Function getter) { return this.get().fieldOf(fieldName).forGetter(getter); } /** * A helper for defining a field of this type in a RecordCodecBuilder. If the field is not present the fallback * value will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodecBuilder. * @return A RecordCodecBuilder that represents a field of the helpers type with a fallback value. */ public RecordCodecBuilder get(String fieldName, Function getter, T fallback) { return this.get().optionalFieldOf(fieldName, fallback).forGetter(getter); } /** * Gets a codec that can read and write an array. For the sake of convenience single elements are treated as an * array of one. * * @return A Codec that can read and write an array. */ public Codec getArray() { return MapCodecs.flexibleArray(this.get(), this.arrayBuilder); } /** * A helper for defining a field for an array of this type in a RecordCodecBuilder. For the sake of convenience * single elements are treated as an array of one. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for an array. */ public RecordCodecBuilder getArray(String fieldName, Function getter) { return this.getArray().fieldOf(fieldName).forGetter(getter); } /** * A helper for defining a field for an array of this type in a RecordCodecBuilder. For the sake of convenience * single elements are treated as an array of one. If the field is not present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for an array. */ public RecordCodecBuilder getArray(String fieldName, Function getter, T... fallback) { return this.getArray().optionalFieldOf(fieldName, fallback).forGetter(getter); } /** * Gets a codec that can read and write a list. For the sake of convenience single elements are treated as a list of * one. * * @return A Codec that can read and write a list. */ public Codec> getList() { return MapCodecs.flexibleList(this.get()); } /** * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a list of one. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a list. */ public RecordCodecBuilder> getList(String fieldName, Function> getter) { return this.getList().fieldOf(fieldName).forGetter(getter); } /** * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a list of one. If the field is not present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a list. */ public RecordCodecBuilder> getList(String fieldName, Function> getter, List fallback) { return this.getList().optionalFieldOf(fieldName, fallback).forGetter(getter); } /** * A helper for defining a field for a list of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a list of one. If the field is not present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a list. */ public RecordCodecBuilder> getList(String fieldName, Function> getter, T... fallback) { return this.getList().optionalFieldOf(fieldName, List.of(fallback)).forGetter(getter); } /** * Gets a codec that can read and write a set. For the sake of convenience single elements are treated as a list of * one. * * @return A Codec that can read and write a set. */ public Codec> getSet() { return MapCodecs.flexibleSet(this.get()); } /** * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a set of one. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a set. */ public RecordCodecBuilder> getSet(String fieldName, Function> getter) { return this.getSet().fieldOf(fieldName).forGetter(getter); } /** * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a set of one. If the field is not present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a set. */ public RecordCodecBuilder> getSet(String fieldName, Function> getter, Set fallback) { return this.getSet().optionalFieldOf(fieldName, fallback).forGetter(getter); } /** * A helper for defining a field for a set of this type in a RecordCodecBuilder. For the sake of convenience single * elements are treated as a set of one. If the field is not present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a set. */ public RecordCodecBuilder> getSet(String fieldName, Function> getter, T... fallback) { return this.getSet().optionalFieldOf(fieldName, Set.of(fallback)).forGetter(getter); } /** * Gets a codec that can read and write an optional value. * * @param fieldName The name of the field to read the value from. * @return A Codec that can read and write an optional value. */ public MapCodec> getOptional(String fieldName) { return this.get().optionalFieldOf(fieldName); } /** * A helper for defining a field for an optional value of this type in a RecordCodecBuilder. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for an optional value. */ public RecordCodecBuilder> getOptional(String fieldName, Function> getter) { return this.get().optionalFieldOf(fieldName).forGetter(getter); } /** * A helper for defining a field for an optional value of this type in a RecordCodecBuilder. If the field is not * present the fallback will be used. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param fallback The fallback value to use when the field is not present. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for an optional value. */ public RecordCodecBuilder> getOptional(String fieldName, Function> getter, Optional fallback) { return MapCodecs.optional(this.get(), fieldName, fallback, true).forGetter(getter); } /** * Gets a codec that can read and write nullable values. * * @param fieldName The name of the field to read the value from. * @return A Codec that can read and write nullable values. */ public MapCodec getNullable(String fieldName) { return MapCodecs.nullable(this.get(), fieldName); } /** * A helper for defining a field for a nullable value of this type in a RecordCodecBuilder. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodedBuilder. * @return A RecordCodecBuilder that represents a field for a nullable value. */ public RecordCodecBuilder getNullable(String fieldName, Function getter) { return this.getNullable(fieldName).forGetter(getter); } /** * Gets a codec that can read and write a weighted entry. * * @return A Codec that can read and write a weighted entry. */ public Codec> getWeighted() { return WeightedEntry.Wrapper.codec(this.get()); } /** * A helper for defining a field for a weighted entry of this type in a RecordCodecBuilder. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodecBuilder. * @return A RecordCodecBuilder that represents a field for a weighted entry. */ public RecordCodecBuilder> getWeighted(String fieldName, Function> getter) { return WeightedEntry.Wrapper.codec(this.get()).fieldOf(fieldName).forGetter(getter); } /** * Gets a codec that can read and write a weighted list. * * @return A Codec that can read and write a weighted list. */ public Codec> getWeightedList() { return SimpleWeightedRandomList.wrappedCodec(this.get()); } /** * A helper for defining a field for a weighted list of this type in a RecordCodecBuilder. * * @param fieldName The name of the field to read the value from. * @param getter A getter that will read the value from an object of the RecordCodecBuilders type. * @param The type of the RecordCodecBuilder. * @return A RecordCodecBuilder that represents a field for a weighted list. */ public RecordCodecBuilder> getWeightedList(String fieldName, Function> getter) { return this.getWeightedList().fieldOf(fieldName).forGetter(getter); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/MapCodecs.java ================================================ package net.darkhax.bookshelf.common.api.data.codecs.map; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.util.FunctionHelper; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.Optionull; import net.minecraft.advancements.CriterionTrigger; import net.minecraft.advancements.critereon.EntitySubPredicate; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Holder; import net.minecraft.core.UUIDUtil; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.particles.ParticleType; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.network.chat.numbers.NumberFormatType; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; import net.minecraft.stats.StatType; import net.minecraft.util.ExtraCodecs; import net.minecraft.util.valueproviders.FloatProviderType; import net.minecraft.util.valueproviders.IntProviderType; import net.minecraft.world.Difficulty; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.MobCategory; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.memory.MemoryModuleType; import net.minecraft.world.entity.ai.sensing.SensorType; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.animal.CatVariant; import net.minecraft.world.entity.animal.FrogVariant; import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.entity.npc.VillagerType; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.entity.schedule.Schedule; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.ArmorMaterial; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.DyeColor; import net.minecraft.world.item.Instrument; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Rarity; import net.minecraft.world.item.alchemy.Potion; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.item.enchantment.LevelBasedValue; import net.minecraft.world.item.enchantment.effects.EnchantmentEntityEffect; import net.minecraft.world.item.enchantment.effects.EnchantmentLocationBasedEffect; import net.minecraft.world.item.enchantment.effects.EnchantmentValueEffect; import net.minecraft.world.item.enchantment.providers.EnchantmentProvider; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.DecoratedPotPattern; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.PositionSourceType; import net.minecraft.world.level.levelgen.DensityFunction; import net.minecraft.world.level.levelgen.SurfaceRules; import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicateType; import net.minecraft.world.level.levelgen.carver.WorldCarver; import net.minecraft.world.level.levelgen.feature.Feature; import net.minecraft.world.level.levelgen.feature.featuresize.FeatureSizeType; import net.minecraft.world.level.levelgen.feature.foliageplacers.FoliagePlacerType; import net.minecraft.world.level.levelgen.feature.rootplacers.RootPlacerType; import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProviderType; import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecoratorType; import net.minecraft.world.level.levelgen.feature.trunkplacers.TrunkPlacerType; import net.minecraft.world.level.levelgen.heightproviders.HeightProviderType; import net.minecraft.world.level.levelgen.placement.PlacementModifierType; import net.minecraft.world.level.levelgen.structure.StructureType; import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType; import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType; import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType; import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; import net.minecraft.world.level.levelgen.structure.templatesystem.PosRuleTestType; import net.minecraft.world.level.levelgen.structure.templatesystem.RuleTestType; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorType; import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.RuleBlockEntityModifierType; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.saveddata.maps.MapDecorationType; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType; import net.minecraft.world.level.storage.loot.functions.LootItemFunctionType; import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; import net.minecraft.world.level.storage.loot.providers.nbt.LootNbtProviderType; import net.minecraft.world.level.storage.loot.providers.number.LootNumberProviderType; import net.minecraft.world.level.storage.loot.providers.score.LootScoreProviderType; import org.joml.Vector3f; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; import java.util.UUID; import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @SuppressWarnings("unused") public class MapCodecs { // JAVA TYPES public static final MapCodecHelper BOOLEAN = new MapCodecHelper<>(Codec.BOOL); public static final MapCodecHelper BYTE = new MapCodecHelper<>(Codec.BYTE); public static final MapCodecHelper SHORT = new MapCodecHelper<>(Codec.SHORT); public static final MapCodecHelper INT = new MapCodecHelper<>(Codec.INT); public static final MapCodecHelper FLOAT = new MapCodecHelper<>(Codec.FLOAT); public static final MapCodecHelper LONG = new MapCodecHelper<>(Codec.LONG); public static final MapCodecHelper DOUBLE = new MapCodecHelper<>(Codec.DOUBLE); public static final MapCodecHelper STRING = new MapCodecHelper<>(Codec.STRING); public static final MapCodecHelper UUID = new MapCodecHelper<>(UUIDUtil.CODEC); // REGISTRIES public static final MapCodecHelper> GAME_EVENT = RegistryMapCodecHelper.create(BuiltInRegistries.GAME_EVENT); public static final MapCodecHelper> SOUND_EVENT = RegistryMapCodecHelper.create(BuiltInRegistries.SOUND_EVENT); public static final MapCodecHelper> FLUID = RegistryMapCodecHelper.create(BuiltInRegistries.FLUID); public static final MapCodecHelper> MOB_EFFECT = RegistryMapCodecHelper.create(BuiltInRegistries.MOB_EFFECT); public static final MapCodecHelper> BLOCK = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK); public static final MapCodecHelper>> ENTITY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENTITY_TYPE); public static final MapCodecHelper> ITEM = RegistryMapCodecHelper.create(BuiltInRegistries.ITEM); public static final MapCodecHelper> POTION = RegistryMapCodecHelper.create(BuiltInRegistries.POTION); public static final MapCodecHelper>> PARTICLE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.PARTICLE_TYPE); public static final MapCodecHelper>> BLOCK_ENTITY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_ENTITY_TYPE); public static final MapCodecHelper> CUSTOM_STAT = RegistryMapCodecHelper.create(BuiltInRegistries.CUSTOM_STAT); public static final MapCodecHelper> CHUNK_STATUS = RegistryMapCodecHelper.create(BuiltInRegistries.CHUNK_STATUS); public static final MapCodecHelper>> RULE_TEST = RegistryMapCodecHelper.create(BuiltInRegistries.RULE_TEST); public static final MapCodecHelper>> RULE_BLOCK_ENTITY_MODIFIER = RegistryMapCodecHelper.create(BuiltInRegistries.RULE_BLOCK_ENTITY_MODIFIER); public static final MapCodecHelper>> POS_RULE_TEST = RegistryMapCodecHelper.create(BuiltInRegistries.POS_RULE_TEST); public static final MapCodecHelper>> MENU = RegistryMapCodecHelper.create(BuiltInRegistries.MENU); public static final MapCodecHelper>> RECIPE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.RECIPE_TYPE); public static final MapCodecHelper>> RECIPE_SERIALIZER = RegistryMapCodecHelper.create(BuiltInRegistries.RECIPE_SERIALIZER); public static final MapCodecHelper> ATTRIBUTE = RegistryMapCodecHelper.create(BuiltInRegistries.ATTRIBUTE); public static final MapCodecHelper>> POSITION_SOURCE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POSITION_SOURCE_TYPE); public static final MapCodecHelper>> COMMAND_ARGUMENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.COMMAND_ARGUMENT_TYPE); public static final MapCodecHelper>> STAT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.STAT_TYPE); public static final MapCodecHelper> VILLAGER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.VILLAGER_TYPE); public static final MapCodecHelper> VILLAGER_PROFESSION = RegistryMapCodecHelper.create(BuiltInRegistries.VILLAGER_PROFESSION); public static final MapCodecHelper> POINT_OF_INTEREST_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POINT_OF_INTEREST_TYPE); public static final MapCodecHelper>> MEMORY_MODULE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.MEMORY_MODULE_TYPE); public static final MapCodecHelper>> SENSOR_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.SENSOR_TYPE); public static final MapCodecHelper> SCHEDULE = RegistryMapCodecHelper.create(BuiltInRegistries.SCHEDULE); public static final MapCodecHelper> ACTIVITY = RegistryMapCodecHelper.create(BuiltInRegistries.ACTIVITY); public static final MapCodecHelper> LOOT_POOL_ENTRY_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE); public static final MapCodecHelper>> LOOT_FUNCTION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_FUNCTION_TYPE); public static final MapCodecHelper> LOOT_CONDITION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_CONDITION_TYPE); public static final MapCodecHelper> LOOT_NUMBER_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_NUMBER_PROVIDER_TYPE); public static final MapCodecHelper> LOOT_NBT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_NBT_PROVIDER_TYPE); public static final MapCodecHelper> LOOT_SCORE_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.LOOT_SCORE_PROVIDER_TYPE); public static final MapCodecHelper>> FLOAT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FLOAT_PROVIDER_TYPE); public static final MapCodecHelper>> INT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.INT_PROVIDER_TYPE); public static final MapCodecHelper>> HEIGHT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.HEIGHT_PROVIDER_TYPE); public static final MapCodecHelper>> BLOCK_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_PREDICATE_TYPE); public static final MapCodecHelper>> CARVER = RegistryMapCodecHelper.create(BuiltInRegistries.CARVER); public static final MapCodecHelper>> FEATURE = RegistryMapCodecHelper.create(BuiltInRegistries.FEATURE); public static final MapCodecHelper>> STRUCTURE_PLACEMENT = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PLACEMENT); public static final MapCodecHelper> STRUCTURE_PIECE = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PIECE); public static final MapCodecHelper>> STRUCTURE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_TYPE); public static final MapCodecHelper>> PLACEMENT_MODIFIER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.PLACEMENT_MODIFIER_TYPE); public static final MapCodecHelper>> BLOCKSTATE_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCKSTATE_PROVIDER_TYPE); public static final MapCodecHelper>> FOLIAGE_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FOLIAGE_PLACER_TYPE); public static final MapCodecHelper>> TRUNK_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.TRUNK_PLACER_TYPE); public static final MapCodecHelper>> ROOT_PLACER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ROOT_PLACER_TYPE); public static final MapCodecHelper>> TREE_DECORATOR_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.TREE_DECORATOR_TYPE); public static final MapCodecHelper>> FEATURE_SIZE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.FEATURE_SIZE_TYPE); public static final MapCodecHelper>> BIOME_SOURCE = RegistryMapCodecHelper.create(BuiltInRegistries.BIOME_SOURCE); public static final MapCodecHelper>> CHUNK_GENERATOR = RegistryMapCodecHelper.create(BuiltInRegistries.CHUNK_GENERATOR); public static final MapCodecHelper>> MATERIAL_CONDITION = RegistryMapCodecHelper.create(BuiltInRegistries.MATERIAL_CONDITION); public static final MapCodecHelper>> MATERIAL_RULE = RegistryMapCodecHelper.create(BuiltInRegistries.MATERIAL_RULE); public static final MapCodecHelper>> DENSITY_FUNCTION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.DENSITY_FUNCTION_TYPE); public static final MapCodecHelper>> BLOCK_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.BLOCK_TYPE); public static final MapCodecHelper>> STRUCTURE_PROCESSOR = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_PROCESSOR); public static final MapCodecHelper>> STRUCTURE_POOL_ELEMENT = RegistryMapCodecHelper.create(BuiltInRegistries.STRUCTURE_POOL_ELEMENT); public static final MapCodecHelper>> POOL_ALIAS_BINDING_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.POOL_ALIAS_BINDING_TYPE); public static final MapCodecHelper> CAT_VARIANT = RegistryMapCodecHelper.create(BuiltInRegistries.CAT_VARIANT); public static final MapCodecHelper> FROG_VARIANT = RegistryMapCodecHelper.create(BuiltInRegistries.FROG_VARIANT); public static final MapCodecHelper> INSTRUMENT = RegistryMapCodecHelper.create(BuiltInRegistries.INSTRUMENT); public static final MapCodecHelper> DECORATED_POT_PATTERN = RegistryMapCodecHelper.create(BuiltInRegistries.DECORATED_POT_PATTERN); public static final MapCodecHelper> CREATIVE_MODE_TAB = RegistryMapCodecHelper.create(BuiltInRegistries.CREATIVE_MODE_TAB); public static final MapCodecHelper>> TRIGGER_TYPES = RegistryMapCodecHelper.create(BuiltInRegistries.TRIGGER_TYPES); public static final MapCodecHelper>> NUMBER_FORMAT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.NUMBER_FORMAT_TYPE); public static final MapCodecHelper> ARMOR_MATERIAL = RegistryMapCodecHelper.create(BuiltInRegistries.ARMOR_MATERIAL); public static final MapCodecHelper>> DATA_COMPONENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.DATA_COMPONENT_TYPE); public static final MapCodecHelper>> ENTITY_SUB_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENTITY_SUB_PREDICATE_TYPE); public static final MapCodecHelper>> ITEM_SUB_PREDICATE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE); public static final MapCodecHelper> MAP_DECORATION_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.MAP_DECORATION_TYPE); public static final MapCodecHelper>> ENCHANTMENT_EFFECT_COMPONENT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE); public static final MapCodecHelper>> ENCHANTMENT_LEVEL_BASED_VALUE_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_LEVEL_BASED_VALUE_TYPE); public static final MapCodecHelper>> ENCHANTMENT_ENTITY_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_ENTITY_EFFECT_TYPE); public static final MapCodecHelper>> ENCHANTMENT_LOCATION_BASED_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_LOCATION_BASED_EFFECT_TYPE); public static final MapCodecHelper>> ENCHANTMENT_VALUE_EFFECT_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_VALUE_EFFECT_TYPE); public static final MapCodecHelper>> ENCHANTMENT_PROVIDER_TYPE = RegistryMapCodecHelper.create(BuiltInRegistries.ENCHANTMENT_PROVIDER_TYPE); // ENUMS public static final MapCodecHelper ITEM_RARITY = new MapCodecHelper<>(enumerable(Rarity.class)); public static final MapCodecHelper ATTRIBUTE_OPERATION = new MapCodecHelper<>(AttributeModifier.Operation.CODEC); public static final MapCodecHelper DIRECTION = new MapCodecHelper<>(enumerable(Direction.class)); public static final MapCodecHelper AXIS = new MapCodecHelper<>(enumerable(Direction.Axis.class)); public static final MapCodecHelper PLANE = new MapCodecHelper<>(enumerable(Direction.Plane.class)); public static final MapCodecHelper MOB_CATEGORY = new MapCodecHelper<>(enumerable(MobCategory.class)); public static final MapCodecHelper DYE_COLOR = new MapCodecHelper<>(enumerable(DyeColor.class)); public static final MapCodecHelper SOUND_SOURCE = new MapCodecHelper<>(enumerable(SoundSource.class)); public static final MapCodecHelper DIFFICULTY = new MapCodecHelper<>(enumerable(Difficulty.class)); public static final MapCodecHelper EQUIPMENT_SLOT = new MapCodecHelper<>(enumerable(EquipmentSlot.class)); public static final MapCodecHelper MIRROR = new MapCodecHelper<>(enumerable(Mirror.class)); public static final MapCodecHelper ROTATION = new MapCodecHelper<>(enumerable(Rotation.class)); // MINECRAFT TYPES public static final MapCodecHelper RESOURCE_LOCATION = new MapCodecHelper<>(ResourceLocation.CODEC); public static final MapCodecHelper COMPOUND_TAG = new MapCodecHelper<>(CompoundTag.CODEC); public static final MapCodecHelper ITEM_STACK = new MapCodecHelper<>(ItemStack.CODEC); public static final MapCodecHelper ITEM_STACK_STRICT = new MapCodecHelper<>(ItemStack.STRICT_CODEC); public static final MapCodecHelper TEXT = new MapCodecHelper<>(ComponentSerialization.CODEC); public static final MapCodecHelper BLOCK_POS = new MapCodecHelper<>(BlockPos.CODEC); public static final MapCodecHelper INGREDIENT = new MapCodecHelper<>(Ingredient.CODEC); public static final MapCodecHelper INGREDIENT_NONEMPTY = new MapCodecHelper<>(Ingredient.CODEC_NONEMPTY); public static final MapCodec BLOCK_STATE_MAP_CODEC = Codec.mapPair(BLOCK.get().fieldOf("block"), Codec.unboundedMap(Codec.STRING, Codec.STRING).optionalFieldOf("properties")).flatXmap(MapCodecs::decodeBlockState, MapCodecs::encodeBlockState); public static final MapCodecHelper BLOCK_STATE = new MapCodecHelper<>(BLOCK_STATE_MAP_CODEC.codec()); public static final MapCodecHelper ATTRIBUTE_MODIFIER = new MapCodecHelper<>(AttributeModifier.CODEC); public static final MapCodecHelper EFFECT_INSTANCE = new MapCodecHelper<>(MobEffectInstance.CODEC); public static final MapCodecHelper VECTOR_3F = new MapCodecHelper<>(ExtraCodecs.VECTOR3F); // Bookshelf Types public static final MapCodecHelper LOAD_CONDITION = LoadConditions.CODEC_HELPER; /** * Creates a Codec that can flexibly read individual values as a list in addition to traditional lists. * * @param codec The codec for reading an individual value. * @param The type of value handled by the codec. * @return A Codec that can flexibly read individual values as a list in addition to traditional lists. */ public static Codec> flexibleList(Codec codec) { return Codec.either(codec.listOf(), codec).xmap(either -> either.map(Function.identity(), List::of), list -> list.size() == 1 ? Either.right(list.getFirst()) : Either.left(list)); } /** * Creates a Codec that can flexibly read both individual values and arrays of values as a set. * * @param codec The Codec for reading an individual value. * @param The type of value handled by the codec. * @return A Codec that can flexibly read both individual values and arrays of values as a set. */ public static Codec> flexibleSet(Codec codec) { return flexibleList(codec).xmap(LinkedHashSet::new, ArrayList::new); } /** * Creates a Codec that can flexibly read both individual values and arrays of values as an array. * * @param codec The Codec for reading an individual value. * @param arrayBuilder A function that creates new arrays of the required type. The function is given the size of * the list. * @param The type of value handled by the codec. * @return A Codec that can flexibly read both individual values and arrays of values as an array. */ public static Codec flexibleArray(Codec codec, IntFunction arrayBuilder) { return flexibleList(codec).xmap(list -> list.toArray(arrayBuilder.apply(list.size())), List::of); } /** * Creates a Codec that will use a fallback value if no other value is specified. This is different from * {@link Codec#optionalFieldOf(String, Object)} in that the fallback value is provided by a supplier. * * @param codec The base Codec to use. * @param name The name of the field to read from. * @param fallbackSupplier A supplier that produces the default value. You should probably memoize this. * @param The type of value handled by the codec. * @return A Codec that will use a fallback value if the field is not specified. */ public static MapCodec fallback(Codec codec, String name, Supplier fallbackSupplier) { return fallback(codec, name, fallbackSupplier, true); } /** * Creates a Codec that handles optional values. This is different from * {@link Codec#optionalFieldOf(String, Object)} in that it keeps the type as an Optional. * * @param codec The base Codec to use. * @param name The name of the field to read from. * @param fallback The fallback optional value. * @param writesDefault Should the default value be written or left blank? * @param The type of the optional value handled by the coded. * @return A Codec that handles optional values. */ public static MapCodec> optional(Codec codec, String name, Optional fallback, boolean writesDefault) { return Codec.optionalField(name, codec, false).xmap(o -> o.isPresent() ? o : fallback, a -> a.isEmpty() || (Objects.equals(a.get(), fallback.orElse(null)) && !writesDefault) ? Optional.empty() : a); } /** * Creates a Codec that can handle nullable values. * * @param codec The base Codec to use. * @param fieldName The name of the field to read from. * @param The type of value handled by the codec. * @return A Codec that handles nullable values. */ public static MapCodec nullable(Codec codec, String fieldName) { return Codec.optionalField(fieldName, codec, false).xmap(optional -> optional.orElse(null), Optional::ofNullable); } /** * Creates a Codec that will use a fallback value if no other value is specified. This is different from * {@link Codec#optionalFieldOf(String, Object)} in that the fallback value is provided by a supplier. It also * allows you to control if the default value should be written when or left blank. * * @param codec The base Codec to use. * @param name The name of the field to read from. * @param fallbackSupplier A supplier that produces the default value. You should probably memoize this. * @param writesDefault Should the default value be written or left blank? * @param The type of value handled by the codec. * @return A Codec that will use a fallback value if the field is not specified. */ public static MapCodec fallback(Codec codec, String name, Supplier fallbackSupplier, boolean writesDefault) { return Codec.optionalField(name, codec, false).xmap(value -> value.orElse(fallbackSupplier.get()), value -> { final T fallback = fallbackSupplier.get(); return Objects.equals(value, fallback) && !writesDefault ? Optional.empty() : Optional.of(value); }); } private static > Map getEnumsByName(Class enumClass) { if (!enumClass.isEnum()) { throw new IllegalStateException("Class " + enumClass.getCanonicalName() + " is not an enum!"); } final Map valueMap = new HashMap<>(); for (T value : enumClass.getEnumConstants()) { final String name = value.name(); if (valueMap.containsKey(name)) { Constants.LOG.error("Duplicate name '{}' found in enum '{}'. Another mod is doing something very wrong. old='{}' new='{}'", name, enumClass.getName(), valueMap.get(name), value); } valueMap.put(name, value); } return valueMap; } /** * Creates a Codec that handles enum values by using their enum constant names. *
* If the codec can not read an enum value from the provided name it will try again using an all uppercase version * of the input. This allows users to write values in all lowercase which may feel more natural for some users and * adds extra flexibility. *
* If the codec can not read any enum value from the provided name it will create an error. The error message will * try to help the user by recommending a nearby match and including all possible values. * * @param enumClass The class of the enum to get values for. * @param The type of the enum. * @return A codec that can read and write enum values using their enum constant name. */ public static > Codec enumerable(Class enumClass) { final Map enumValues = getEnumsByName(enumClass); final Function fromString = name -> { T value = enumValues.get(name); if (value == null) { value = enumValues.get(name.toUpperCase(Locale.ROOT)); } return value; }; final UnaryOperator errorMessage = name -> { final StringJoiner message = new StringJoiner(" "); message.add("Unable to find " + enumClass.getSimpleName() + " entry \"" + name + "\"."); final Set similarMatches = TextHelper.getPossibleMatches(name, enumValues.keySet(), 2); if (!similarMatches.isEmpty()) { message.add("Did you mean \"" + similarMatches.stream().findFirst().get() + "\"?"); } message.add("Available Options are " + TextHelper.formatCollection(enumValues.keySet())); return message.toString(); }; return Codec.STRING.flatXmap(string -> Optionull.mapOrElse(fromString.apply(string), DataResult::success, () -> DataResult.error(() -> errorMessage.apply(string))), object -> DataResult.success(object.name())); } // INTERNAL HELPERS @SuppressWarnings({"rawtypes", "unchecked"}) private static DataResult decodeBlockState(Pair, Optional>> props) { final Block block = props.getFirst().value(); final Map properties = props.getSecond().orElse(new HashMap<>()); BlockState state = block.defaultBlockState(); if (!properties.isEmpty()) { final StateDefinition definition = block.getStateDefinition(); for (Map.Entry entry : properties.entrySet()) { final Property> property = definition.getProperty(entry.getKey()); if (property != null) { final Optional value = property.getValue(entry.getValue()); if (value.isPresent()) { try { state = state.setValue((Property) property, (Comparable) value.get()); } catch (final Exception e) { Constants.LOG.error("Failed to update state for block {} with valid value {}={}. The mod that adds this block may have a serious issue.", BuiltInRegistries.BLOCK.getKey(block), entry.getKey(), entry.getValue()); return DataResult.error(e::getMessage); } } else { return DataResult.error(() -> "\"" + entry.getValue() + "\" is not a valid value for property \"" + property.getName() + "\" on block \"" + BuiltInRegistries.BLOCK.getKey(block) + "\". Available values: " + property.getAllValues().map(propVal -> ((Property) property).getName(propVal.value())).collect(Collectors.joining())); } } else { return DataResult.error(() -> "The property \"" + entry.getKey() + "\" is not valid for block \"" + BuiltInRegistries.BLOCK.getKey(block) + "\". Available properties: " + definition.getProperties().stream().map(Property::getName).collect(Collectors.joining())); } } } return DataResult.success(state); } @SuppressWarnings({"rawtypes", "unchecked"}) private static DataResult, Optional>>> encodeBlockState(BlockState state) { final Map propertyMap = new HashMap<>(); for (Map.Entry, Comparable> entry : state.getValues().entrySet()) { propertyMap.put(entry.getKey().getName(), ((Property) entry.getKey()).getName(entry.getValue())); } return DataResult.success(new Pair<>(state.getBlock().builtInRegistryHolder(), Optional.ofNullable(propertyMap.isEmpty() ? null : propertyMap))); } /** * Creates a codec that will try two different codecs, using the first valid codec. Encoding will always use the * first codec. * * @param first The first codec to try when decoding. This will be the only codec used in encoding. * @param second The second codec to try when decoding. * @param The type of codec to create. * @return A codec that will try two different codecs. */ public static Codec xor(Codec first, Codec second) { return Codec.xor(first, second).xmap(FunctionHelper::unpack, Either::left); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/map/RegistryMapCodecHelper.java ================================================ package net.darkhax.bookshelf.common.api.data.codecs.map; import com.mojang.serialization.Codec; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.resources.RegistryFixedCodec; import net.minecraft.resources.ResourceKey; import net.minecraft.tags.TagKey; public class RegistryMapCodecHelper extends MapCodecHelper> { private final MapCodecHelper> tagHelper; private RegistryMapCodecHelper(Codec> holderCodec, ResourceKey> key) { super(holderCodec); this.tagHelper = new MapCodecHelper<>(TagKey.codec(key)); } public MapCodecHelper> tag() { return this.tagHelper; } /** * Creates a Codec helper for a builtin registry. * * @param registry The registry to create a codec helper for. * @param The type of value held by the registry. * @return A Codec helper for a builtin registry. */ public static RegistryMapCodecHelper create(Registry registry) { return new RegistryMapCodecHelper<>(registry.holderByNameCodec(), (ResourceKey>) registry.key()); } /** * Creates a Codec helper for a datapack registry. This codec can only be used when registry access is available * through RegistryOps. * * @param key The key of the registry to use. * @param The type of value held by the registry. * @return A Codec helper for datapack entries. */ public static RegistryMapCodecHelper create(ResourceKey> key) { return new RegistryMapCodecHelper<>(RegistryFixedCodec.create(key), key); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/codecs/stream/StreamCodecs.java ================================================ package net.darkhax.bookshelf.common.api.data.codecs.stream; import io.netty.buffer.ByteBuf; import net.darkhax.bookshelf.common.impl.data.ingredient.FalseIngredient; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import java.util.ArrayList; import java.util.List; public class StreamCodecs { public static final StreamCodec STRING = StreamCodec.of(FriendlyByteBuf::writeUtf, FriendlyByteBuf::readUtf); public static final StreamCodec> ITEM_STACK_LIST = list(ItemStack.STREAM_CODEC); public static final StreamCodec INGREDIENT_NON_EMPTY = StreamCodec.of( Ingredient.CONTENTS_STREAM_CODEC, buf -> { final Ingredient ingredient = Ingredient.CONTENTS_STREAM_CODEC.decode(buf); return ingredient.isEmpty() ? FalseIngredient.INSTANCE.get() : ingredient; } ); public static StreamCodec> list(StreamCodec baseCodec) { return StreamCodec.of( (buf, val) -> { buf.writeInt(val.size()); for (V entry : val) { baseCodec.encode(buf, entry); } }, buf -> { final int size = buf.readInt(); final List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(baseCodec.decode(buf)); } return list; } ); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/ConditionType.java ================================================ package net.darkhax.bookshelf.common.api.data.conditions; import com.mojang.serialization.MapCodec; import net.minecraft.resources.ResourceLocation; /** * Represents a type of load condition that Bookshelf can process and test. * * @param id The ID of the condition type. * @param codec The codec used to serialize the condition from data. */ public record ConditionType(ResourceLocation id, MapCodec codec) { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/ILoadCondition.java ================================================ package net.darkhax.bookshelf.common.api.data.conditions; /** * Load conditions allow JSON entries in data/resource packs to define optional conditions in order for them to load. * For example a recipe file can prevent loading if a required item is not registered. */ public interface ILoadCondition { /** * Tests if the condition has been met or not. * * @return Has the condition been met? */ boolean allowLoading(); /** * Gets the type of the condition. This is required for serializing conditions. * * @return The type of the condition. */ ConditionType getType(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/conditions/LoadConditions.java ================================================ package net.darkhax.bookshelf.common.api.data.conditions; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecHelper; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; public class LoadConditions { private static final Map CONDITION_TYPES = new HashMap<>(); private static final Codec CONDITION_TYPE_CODEC = ResourceLocation.CODEC.xmap(CONDITION_TYPES::get, ConditionType::id); public static final String LOAD_CONDITION_TAG = Constants.id("load_conditions").toString(); public static final Codec CONDITION_CODEC = CONDITION_TYPE_CODEC.dispatch(ILoadCondition::getType, ConditionType::codec); public static final MapCodecHelper CODEC_HELPER = new MapCodecHelper<>(CONDITION_CODEC); @Nullable public static ConditionType getType(ResourceLocation id) { return CONDITION_TYPES.get(id); } public static ConditionType register(ResourceLocation id, MapCodec codec) { if (CONDITION_TYPES.containsKey(id)) { Constants.LOG.warn("JSON Load Serializer ID {} has already been assigned to {}. Replacing with {}.", id, CONDITION_TYPES.get(id).codec(), codec); } final ConditionType type = new ConditionType(id, codec); CONDITION_TYPES.put(id, type); return type; } /** * Reads one or more conditions from a JSON element. If the element is an object an array of 1 will be returned. * * @param conditionData The condition data read from the raw JSON entry. * @return An array of load conditions read from the data. */ public static ILoadCondition[] getConditions(JsonElement conditionData) { return MapCodecs.LOAD_CONDITION.getArray().decode(JsonOps.INSTANCE, conditionData).getOrThrow().getFirst(); } /** * Tests if a raw JSON element can be loaded. This will search for the condition property and attempt to deserialize * and test those conditions. * * @param rawJson The raw JSON data as read from the data/resource pack. * @return Whether the JSON entry should be loaded or not. */ public static boolean canLoad(JsonObject rawJson) { if (rawJson.has(LOAD_CONDITION_TAG)) { final JsonElement conditionData = rawJson.get(LOAD_CONDITION_TAG); for (ILoadCondition condition : getConditions(conditionData)) { if (!condition.allowLoading()) { return false; } } } return true; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/enchantment/EnchantmentLevel.java ================================================ package net.darkhax.bookshelf.common.api.data.enchantment; import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.minecraft.core.Holder; import net.minecraft.tags.TagKey; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.item.enchantment.ItemEnchantments; import java.util.function.ToIntBiFunction; /** * Calculates enchantment levels using different methods. */ public enum EnchantmentLevel { /** * Returns the highest level among all matching enchantments. */ HIGHEST((tag, enchantments) -> { int level = 0; for (Object2IntMap.Entry> entry : enchantments.entrySet()) { if (entry.getKey().is(tag) && entry.getIntValue() > level) { level = entry.getIntValue(); } } return level; }), /** * Returns the lowest level among all matching enchantments. */ LOWEST((tag, enchantments) -> { int level = Integer.MAX_VALUE; for (Object2IntMap.Entry> entry : enchantments.entrySet()) { if (entry.getKey().is(tag) && entry.getIntValue() < level) { level = entry.getIntValue(); } } return level; }), /** * Returns the level of the first matching enchantment. */ FIRST((tag, enchantments) -> { for (Object2IntMap.Entry> entry : enchantments.entrySet()) { if (entry.getKey().is(tag)) { return entry.getIntValue(); } } return 0; }), /** * Returns the combined level of all matching enchantments. */ CUMULATIVE((tag, enchantments) -> { int level = 0; for (Object2IntMap.Entry> entry : enchantments.entrySet()) { if (entry.getKey().is(tag)) { level += entry.getIntValue(); } } return level; }); private final ToIntBiFunction, ItemEnchantments> func; EnchantmentLevel(ToIntBiFunction, ItemEnchantments> func) { this.func = func; } /** * Gets the level of matching enchantments based on the calculation type. * * @param enchType A tag of the enchantments to match on. * @param stack The item to test. * @return The level based on the calculation type. */ public int get(TagKey enchType, ItemStack stack) { return (!stack.isEmpty() && stack.isEnchanted()) ? this.func.applyAsInt(enchType, stack.getEnchantments()) : 0; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/ingredient/IngredientLogic.java ================================================ package net.darkhax.bookshelf.common.api.data.ingredient; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import java.util.ArrayList; import java.util.List; public interface IngredientLogic> { boolean test(ItemStack stack); default List getAllMatchingStacks() { final List matching = new ArrayList<>(); for (Item item : BuiltInRegistries.ITEM) { final ItemStack stack = item.getDefaultInstance(); if (this.test(stack)) { matching.add(stack); } } return matching; } default boolean requiresTesting() { return true; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/loot/PoolTarget.java ================================================ package net.darkhax.bookshelf.common.api.data.loot; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.BuiltInLootTables; import net.minecraft.world.level.storage.loot.LootTable; /** * Represents a specific loot pool target within a loot table. This class also provides predefined constants for some of * the commonly modified loot pools. * * @param table The id of the loot table to target. * @param index The index of the pool within the loot table. This is usually based on the order pools appear in the JSON * data. * @param hash A hash of the pools JSON data. This can be obtained using the bookshelf debug command in a development * environment. */ public record PoolTarget(ResourceLocation table, int index, int hash) { public static final PoolTarget MINESHAFT_RARE = of(BuiltInLootTables.ABANDONED_MINESHAFT, 0, 1537257923); public static final PoolTarget MINESHAFT_UNCOMMON = of(BuiltInLootTables.ABANDONED_MINESHAFT, 1, -444048389); public static final PoolTarget MINESHAFT_COMMON = of(BuiltInLootTables.ABANDONED_MINESHAFT, 2, 634581377); public static final PoolTarget SIMPLE_DUNGEON_RARE = of(BuiltInLootTables.SIMPLE_DUNGEON, 0, -66091299); public static final PoolTarget SIMPLE_DUNGEON_UNCOMMON = of(BuiltInLootTables.SIMPLE_DUNGEON, 1, 1870100239); public static final PoolTarget SIMPLE_DUNGEON_COMMON = of(BuiltInLootTables.SIMPLE_DUNGEON, 2, 2004993944); public static final PoolTarget CAT_GIFT = of(BuiltInLootTables.CAT_MORNING_GIFT, 0, 234355958); public static final PoolTarget FISHING = of(BuiltInLootTables.FISHING, 0, 1127209674); public static final PoolTarget FISHING_FISH = of(BuiltInLootTables.FISHING_FISH, 0, -190358337); public static final PoolTarget FISHING_JUNK = of(BuiltInLootTables.FISHING_JUNK, 0, 1154453499); public static final PoolTarget FISHING_TREASURE = of(BuiltInLootTables.FISHING_TREASURE, 0, 1729324233); public static final PoolTarget PIGLIN_BARTERING = of(BuiltInLootTables.PIGLIN_BARTERING, 0, 718156885); public static final PoolTarget SNIFFER_DIGGING = of(BuiltInLootTables.SNIFFER_DIGGING, 0, 1185470198); public static final PoolTarget ARCHAEOLOGY_PYRAMID = of(BuiltInLootTables.DESERT_PYRAMID_ARCHAEOLOGY, 0, -1867551069); public static final PoolTarget ARCHAEOLOGY_DESERT_WELL = of(BuiltInLootTables.DESERT_WELL_ARCHAEOLOGY, 0, -1508422416); public static final PoolTarget ARCHAEOLOGY_OCEAN_RUIN_COLD = of(BuiltInLootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY, 0, -1117683719); public static final PoolTarget ARCHAEOLOGY_OCEAN_RUIN_WARM = of(BuiltInLootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY, 0, 153317912); public static final PoolTarget ARCHAEOLOGY_TRAIL_RUINS_COMMON = of(BuiltInLootTables.TRAIL_RUINS_ARCHAEOLOGY_COMMON, 0, 300798809); public static final PoolTarget ARCHAEOLOGY_TRAIL_RUINS_RARE = of(BuiltInLootTables.TRAIL_RUINS_ARCHAEOLOGY_RARE, 0, 1848809003); public static PoolTarget of(ResourceKey table, int index, int hash) { return new PoolTarget(table.location(), index, hash); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/data/loot/modifiers/LootPoolAddition.java ================================================ package net.darkhax.bookshelf.common.api.data.loot.modifiers; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; /** * Represents a loot pool entry that should be added to a loot pool. * * @param id A unique ID for the individual entry. This should be unique to each entry and is used to help identify * and debug entry additions. * @param entry The entry to add to the pool. */ public record LootPoolAddition(ResourceLocation id, LootPoolEntryContainer entry) { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/MerchantTier.java ================================================ package net.darkhax.bookshelf.common.api.entity.villager; public enum MerchantTier { NOVICE(0), APPRENTICE(10), JOURNEYMAN(70), EXPERT(150), MASTER(250); private final int requiredExp; MerchantTier(int requiredExp) { this.requiredExp = requiredExp; } public int getRequiredExp() { return this.requiredExp; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerBuys.java ================================================ package net.darkhax.bookshelf.common.api.entity.villager.trades; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.ItemCost; import net.minecraft.world.item.trading.MerchantOffer; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * A simple villager trade entry that represents an item being bought by the villager. * * @param stackToBuy The item being bought by the villager. * @param emeralds The amount of emeralds to award the player. * @param maxUses The amount of times the trade can be performed before a restocking is required. * @param villagerXp The amount of villager XP to award for performing the trade. * @param priceMultiplier A price multiplier. */ public record VillagerBuys(Supplier stackToBuy, int emeralds, int maxUses, int villagerXp, float priceMultiplier) implements VillagerTrades.ItemListing { @Override public MerchantOffer getOffer(@NotNull Entity entity, @NotNull RandomSource random) { return new MerchantOffer(this.stackToBuy.get(), new ItemStack(Items.EMERALD, this.emeralds), this.maxUses, this.villagerXp, this.priceMultiplier); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerOffers.java ================================================ package net.darkhax.bookshelf.common.api.entity.villager.trades; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.item.trading.MerchantOffer; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * A simple villager trade entry that selects a random offer from an equally weighted array of offers. * * @param offers An equally weighted array of offers. */ public record VillagerOffers(Supplier... offers) implements VillagerTrades.ItemListing { @SafeVarargs public VillagerOffers { } @Override public MerchantOffer getOffer(@NotNull Entity entity, @NotNull RandomSource randomSource) { return offers[randomSource.nextInt(offers.length)].get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/entity/villager/trades/VillagerSells.java ================================================ package net.darkhax.bookshelf.common.api.entity.villager.trades; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.ItemCost; import net.minecraft.world.item.trading.MerchantOffer; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * A simple villager trade entry that represents an item being sold to the player. * * @param itemToBuy The item being sold by the villager. * @param emeraldCost The amount of emeralds the trade will cost. * @param maxUses The amount of times the trade can be performed before a restocking is required. * @param villagerXp The amount of villager XP to award for performing the trade. * @param priceMultiplier A price multiplier. */ public record VillagerSells(Supplier itemToBuy, int emeraldCost, int maxUses, int villagerXp, float priceMultiplier) implements VillagerTrades.ItemListing { @Override public MerchantOffer getOffer(@NotNull Entity villager, @NotNull RandomSource rng) { return new MerchantOffer(new ItemCost(Items.EMERALD, this.emeraldCost), this.itemToBuy.get(), this.maxUses, this.villagerXp, this.priceMultiplier); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/CachedSupplier.java ================================================ package net.darkhax.bookshelf.common.api.function; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; import java.util.function.Supplier; /** * A Supplier implementation that will cache the result of an internal delegate supplier. * * @param The type that is cached by the supplier. */ public class CachedSupplier implements Supplier { /** * The internal Supplier responsible for producing the cached value. */ private final Supplier delegate; /** * Tracks if the delegate supplier has been cached. */ private boolean cached = false; /** * The most recently cached value. */ @Nullable private T cachedValue; protected CachedSupplier(Supplier delegate) { this.delegate = delegate; } @Override public T get() { if (!this.isCached()) { this.cachedValue = this.delegate.get(); this.cached = true; } return cachedValue; } /** * Invalidates the cached value. This will result in a new value being cached the next type {@link #get()} is used. */ public void invalidate() { this.cached = false; this.cachedValue = null; } /** * Checks if this supplier has a cached value. This is not a substitute for null checking. * * @return Has the supplier cached a value. */ public boolean isCached() { return this.cached; } /** * Safely attempts to invoke a consumer with the cached value. If a value has not been cached the consumer will not * be invoked and a new value will not be cached. The consumer will still be invoked if the cached value is null. * * @param consumer The consumer to invoke if a value has been cached. */ public void ifCached(Consumer consumer) { if (this.isCached()) { consumer.accept(this.get()); } } /** * Safely attempts to invoke a consumer with the cached value. If a value has not been cached, or the cached value * is null the consumer will not be invoked and a new value will not be cached. * * @param consumer The consumer to invoke if a cached value is present. */ public void ifPresent(Consumer consumer) { if (this.cachedValue != null) { consumer.accept(this.get()); } } /** * Invokes the consumer with the cached value. This will cause a value to be cached if one has not been cached * already. * * @param consumer The consumer to invoke. */ public void apply(Consumer consumer) { consumer.accept(this.get()); } /** * Performs an unsafe cast to the expected type. */ public CachedSupplier cast() { return (CachedSupplier) this; } /** * Creates a cached supplier that can only produce a single value. * * @param singleton The only value for the cache to use. * @param The type of value held by the cache. * @return A cached supplier that will only produce a single cached value. */ public static CachedSupplier singleton(T singleton) { return cache(() -> singleton); } /** * Creates a cached supplier that will cache a value from the supplied delegate when queried. * * @param delegate The delegate supplier responsible for producing the cached value. This will only be accessed when * the cached supplier is being accessed and has not been cached already. * @param The type of value held by the cache. * @return A supplier that will cache a value from the supplied delegate supplier. */ public static CachedSupplier cache(Supplier delegate) { return new CachedSupplier<>(delegate); } @SuppressWarnings("unchecked") public static CachedSupplier of(ResourceKey key) { return CachedSupplier.cache(() -> { final Registry registry = BuiltInRegistries.REGISTRY.get(key.registry()); if (registry == null) { Constants.LOG.error("Registry {} could not be found!", key.registry()); throw new IllegalStateException("Registry with name " + key.registry() + " was not found!"); } return ((Registry) registry).getOrThrow(key); }); } public static CachedSupplier of(Registry registry, String namespace, String path) { return of(registry, ResourceLocation.fromNamespaceAndPath(namespace, path)); } public static CachedSupplier of(Registry registry, ResourceLocation id) { return CachedSupplier.cache(() -> registry.get(id)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/QuadConsumer.java ================================================ package net.darkhax.bookshelf.common.api.function; import java.util.Objects; /** * A consumer that accepts four parameters. * * @param The first parameter. * @param The second parameter. * @param The third parameter. * @param The fourth parameter. */ @FunctionalInterface public interface QuadConsumer { /** * Consumes the parameters. * * @param p1 The first parameter. * @param p2 The second parameter. * @param p3 The third parameter. * @param p4 The fourth parameter. */ void accept(P1 p1, P2 p2, P3 p3, P4 p4); /** * Chains another consumer on to this one. * * @param after The consumer to run after this one. * @return A new consumer that chains both. */ default QuadConsumer andThen(QuadConsumer after) { Objects.requireNonNull(after); return (p1, p2, p3, p4) -> { accept(p1, p2, p3, p4); after.accept(p1, p2, p3, p4); }; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/ReloadableCache.java ================================================ package net.darkhax.bookshelf.common.api.function; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.common.mixin.access.level.AccessorRecipeManager; import net.minecraft.core.Registry; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; /** * A cached value that is lazily loaded and will be invalidated automatically after the game has been reloaded. * * @param The type of the cached value. */ public class ReloadableCache implements Function { /** * A reloadable cache that will always return null. Not all empty instances will match this instance. */ @SuppressWarnings("rawtypes") public static final ReloadableCache EMPTY = ReloadableCache.of(level -> null); /** * An internal function that is responsible for producing the value to cache. */ private final Function delegate; /** * A flag that tracks if a value has been cached. */ private boolean cached = false; private int revision = 0; /** * The value that is currently cached. */ @Nullable private T cachedValue; protected ReloadableCache(Function delegate) { this.delegate = delegate; } @Nullable @Override public T apply(Level level) { if (!this.isCached() || this.revision != (level.isClientSide ? Constants.CLIENT_REVISION : Constants.SERVER_REVISION)) { this.cachedValue = this.delegate.apply(level); this.revision = (level.isClientSide) ? Constants.CLIENT_REVISION : Constants.SERVER_REVISION; this.cached = true; } return this.cachedValue; } /** * Manually invalidates the cache. This will result in a new value being cached the next time {@link #apply(Level)} * is invoked. */ public void invalidate() { this.cached = false; this.cachedValue = null; this.revision = -1; } /** * Checks if the cache has cached a value. This is not a substitute for null checking. * * @return Has the supplier cached a value. */ public boolean isCached() { return this.cached; } /** * Invokes the consumer with the cached value. This will cause a value to be cached if one has not been cached * already. * * @param level The current game level. This is used to provide context about the current state of the game. * @param consumer The consumer to invoke. */ public void apply(Level level, Consumer consumer) { consumer.accept(this.apply(level)); } /** * Applies a function to the cached value if the value is not null. * * @param level The current game level. This is used to provide context about the current state of the game. * @param consumer The consumer to invoke. */ public void ifPresent(Level level, Consumer consumer) { final T value = this.apply(level); if (value != null) { consumer.accept(value); } } /** * Maps non null cache values to a new value. * * @param level The current game level. This is used to provide context about the current state of the game. * @param mapper A mapper function to map the cached value to something new. This is only used if the value is not * null. * @param The return type. * @return The mapped value or null. */ @Nullable public R map(Level level, Function mapper) { final T value = this.apply(level); return value != null ? mapper.apply(value) : null; } /** * Creates a cache of a value that will be recalculated when the game reloads. * * @param supplier The supplier used to produce the cached value. * @param The type of value held by the cache. * @return A cache of a value that will be recalculated when the game reloads. */ public static ReloadableCache of(Supplier supplier) { return new ReloadableCache<>(level -> supplier.get()); } /** * Creates a cache of a value that will be recalculated when the game reloads. * * @param delegate A function used to produce the value to cache. * @param The type of value held by the cache. * @return A cache of a value that will be recalculated when the game reloads. */ public static ReloadableCache of(Function delegate) { return new ReloadableCache<>(delegate); } /** * Creates a cache of a registry value that will be reaquired when the game reloads. * * @param registry The registry to look up the value in. * @param id The ID of the value to lookup. * @param The type of value held by the cache. * @return A cache of a registry value that will be reaquired when the game reloads. */ public static ReloadableCache of(ResourceKey> registry, ResourceLocation id) { return ReloadableCache.of(level -> level.registryAccess().registryOrThrow(registry).get(id)); } /** * Creates a cache of recipe entries for a recipe type. * * @param type The type of recipe. * @param The type of the recipe. * @return A map of recipes for the recipe type. */ @SuppressWarnings("unchecked") public static > ReloadableCache>> of(RecipeType type) { return ReloadableCache.of(level -> { final Map> byId = new HashMap<>(); if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) { final Collection> recipes = accessor.bookshelf$byTypeMap().get(type); recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder) entry)); } return byId; }); } /** * Creates a cache of recipe entries for a recipe type. * * @param type The type of recipe. * @param The type of the recipe. * @return A map of recipes for the recipe type. */ @SuppressWarnings("unchecked") public static > ReloadableCache>> recipes(Supplier> type) { return ReloadableCache.of(level -> { final Map> byId = new HashMap<>(); if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) { final Collection> recipes = accessor.bookshelf$byTypeMap().get(type.get()); recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder) entry)); } return byId; }); } /** * Creates a cache of an entity instance. * * @param entityData The data used to create the entity. * @return A reloadable entity instance. */ public static ReloadableCache entity(CompoundTag entityData) { if (entityData == null || !entityData.contains("id", Tag.TAG_STRING)) { throw new IllegalStateException("The provided entity data does not contain an entity ID! data=" + entityData); } return ReloadableCache.of(level -> { try { return EntityType.loadEntityRecursive(entityData, level, Function.identity()); } catch (Exception e) { throw new IllegalStateException("Encountered an error while constructing the target entity.", e); } }); } /** * Creates a cache of a living entity instance. * * @param entityData The data used to create the entity. * @return A reloadable living entity instance. */ public static ReloadableCache living(CompoundTag entityData) { final ReloadableCache entityCache = entity(entityData); return ReloadableCache.of(level -> { if (entityCache.apply(level) instanceof LivingEntity living) { return living; } throw new IllegalStateException("Constructed entity was not a LivingEntity type. data=" + entityData); }); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/SidedReloadableCache.java ================================================ package net.darkhax.bookshelf.common.api.function; import net.darkhax.bookshelf.common.mixin.access.level.AccessorRecipeManager; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public class SidedReloadableCache implements Function { private final ReloadableCache client; private final ReloadableCache server; public SidedReloadableCache(ReloadableCache client, ReloadableCache server) { this.client = client; this.server = server; } public ReloadableCache getCache(Level level) { return level.isClientSide ? client : server; } @Nullable @Override public T apply(Level level) { return getCache(level).apply(level); } public void invalidate(Level level) { getCache(level).invalidate(); } public boolean isCached(Level level) { return getCache(level).isCached(); } public void apply(Level level, Consumer consumer) { getCache(level).apply(level, consumer); } public void ifPresent(Level level, Consumer consumer) { getCache(level).ifPresent(level, consumer); } @Nullable public R map(Level level, Function mapper) { return getCache(level).map(level, mapper); } public static SidedReloadableCache of(Function cacheFunc) { return new SidedReloadableCache<>(new ReloadableCache<>(cacheFunc), new ReloadableCache<>(cacheFunc)); } @SuppressWarnings("unchecked") public static > SidedReloadableCache>> recipes(Supplier> type) { return of(level -> { final Map> byId = new HashMap<>(); if (level.getRecipeManager() instanceof AccessorRecipeManager accessor) { final Collection> recipes = accessor.bookshelf$byTypeMap().get(type.get()); recipes.forEach(entry -> byId.put(entry.id(), (RecipeHolder) entry)); } return byId; }); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/TriConsumer.java ================================================ package net.darkhax.bookshelf.common.api.function; import java.util.Objects; @FunctionalInterface public interface TriConsumer { void accept(P1 p1, P2 p2, P3 p3); default TriConsumer andThen(TriConsumer after) { Objects.requireNonNull(after); return (p1, p2, p3) -> { accept(p1, p2, p3); after.accept(p1, p2, p3); }; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/function/TriFunction.java ================================================ package net.darkhax.bookshelf.common.api.function; import java.util.Objects; import java.util.function.Function; @FunctionalInterface public interface TriFunction { R apply(P1 p1, P2 p2, P3 p3); default TriFunction andThen(Function after) { Objects.requireNonNull(after); return (p1, p2, p3) -> after.apply(apply(p1, p2, p3)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/item/IItemHooks.java ================================================ package net.darkhax.bookshelf.common.api.item; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import java.util.function.Consumer; public interface IItemHooks { default void addCreativeTabForms(CreativeModeTab tab, Consumer displayItems) { // NO-OP } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/loot/LootPoolEntryDescriber.java ================================================ package net.darkhax.bookshelf.common.api.loot; import net.minecraft.core.RegistryAccess; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; /** * Describes the potential items that a loot pool entry can generate. See {@link LootPoolEntryDescriptions} for usage. */ @FunctionalInterface public interface LootPoolEntryDescriber { /** * Describes items that may potentially be dropped by a loot pool entry. * * @param registries Access to the current game registries. * @param entry The loot pool entry to be processed. * @param collector Collects entries from the entry into the desired format. */ void getPotentialDrops(@NotNull RegistryAccess registries, @NotNull LootPoolEntryContainer entry, Consumer collector); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/loot/LootPoolEntryDescriptions.java ================================================ package net.darkhax.bookshelf.common.api.loot; import com.mojang.datafixers.util.Either; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack; import net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorCompositeEntryBase; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootItem; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootPool; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorNestedLootTable; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorTagEntry; import net.minecraft.ChatFormatting; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.tags.TagKey; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.ItemLore; import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.entries.DynamicLoot; import net.minecraft.world.level.storage.loot.entries.EmptyLootItem; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; /** * Provides a system for describing what items can be dropped by a loot table. */ public class LootPoolEntryDescriptions { private static final CachedSupplier UNKNOWN_ITEM_DISPLAY = CachedSupplier.cache(() -> { final ItemStack stack = new ItemStack(Items.STRUCTURE_VOID); stack.set(DataComponents.ITEM_NAME, Component.translatable("tooltips.bookshelf.loot.unknown")); stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable("tooltips.bookshelf.loot.unknown.desc").withStyle(ChatFormatting.GRAY)))); return stack; }); private static final CachedSupplier EMPTY_ITEM_DISPLAY = CachedSupplier.cache(() -> { final ItemStack stack = new ItemStack(Items.BARRIER); stack.set(DataComponents.ITEM_NAME, Component.translatable("tooltips.bookshelf.loot.empty")); stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable("tooltips.bookshelf.loot.empty.desc").withStyle(ChatFormatting.GRAY)))); return stack; }); private static final CachedSupplier DYNAMIC_DISPLAY = CachedSupplier.cache(() -> { final ItemStack stack = new ItemStack(Items.JIGSAW); stack.set(DataComponents.ITEM_NAME, Component.translatable("tooltips.bookshelf.loot.dynamic")); stack.set(DataComponents.LORE, new ItemLore(List.of(), List.of(Component.translatable("tooltips.bookshelf.loot.dynamic.desc").withStyle(ChatFormatting.GRAY)))); return stack; }); private static final Map DESCRIBERS = new HashMap<>(); private static boolean hasInitialized = false; public static final LootPoolEntryDescriber EMPTY = (server, entry, collector) -> { if (entry instanceof EmptyLootItem) { collector.accept(EMPTY_ITEM_DISPLAY.get()); } }; public static final LootPoolEntryDescriber ITEM = (server, entry, collector) -> { if (entry instanceof AccessorLootItem accessor) { collector.accept(new ItemStack(accessor.bookshelf$item())); } }; public static final LootPoolEntryDescriber LOOT_TABLE = (server, entry, collector) -> { if (entry instanceof AccessorNestedLootTable accessor) { getPotentialItems(server, accessor.bookshelf$contents(), collector); } }; public static final LootPoolEntryDescriber DYNAMIC = (server, entry, collector) -> { if (entry instanceof DynamicLoot) { collector.accept(DYNAMIC_DISPLAY.get()); } }; public static final LootPoolEntryDescriber TAG = (server, entry, collector) -> { if (entry instanceof AccessorTagEntry tagEntry) { getTagItems(tagEntry.bookshelf$tag(), collector); } }; public static final LootPoolEntryDescriber COMPOSITE = (server, entry, collector) -> { if (entry instanceof AccessorCompositeEntryBase accessor) { getPotentialItems(server, accessor.bookshelf$children(), collector); } }; public static final LootPoolEntryDescriber ITEM_STACK = (server, entry, collector) -> { if (entry instanceof LootItemStack loot) { collector.accept(loot.getBaseStack()); } }; private static void bootstrap() { if (!hasInitialized) { final LootDescriptionAdapter register = new LootDescriptionAdapter(DESCRIBERS::put); Services.CONTENT.get().forEach(provider -> provider.defineLootDescriptions(register)); hasInitialized = true; } } /** * Generates a list of unique items that can generate from a loot table. * * @param registries The current registries. * @param table The loot table to examine. * @return A list containing the entries. */ public static List getUniqueItems(@NotNull RegistryAccess registries, LootTable table) { final List items = new ArrayList<>(); getPotentialItems(registries, table, stack -> addStacking(items, stack)); return items; } /** * Gets potential drops for a loot table. * * @param registries The current registries. * @param table The loot table to examine. * @param consumer Collects entries in your desired format. */ public static void getPotentialItems(@NotNull RegistryAccess registries, Either, LootTable> table, Consumer consumer) { final LootTable resolved = table.map(rl -> registries.registryOrThrow(Registries.LOOT_TABLE).get(rl), Function.identity()); if (resolved != null) { getPotentialItems(registries, resolved, consumer); } } /** * Gets potential drops for a loot table. * * @param registries The current registries. * @param table The loot table to examine. * @param consumer Collects entries in your desired format. */ public static void getPotentialItems(@NotNull RegistryAccess registries, LootTable table, Consumer consumer) { if (table instanceof AccessorLootTable tableAccess) { for (LootPool pool : tableAccess.bookshelf$pools()) { if (pool instanceof AccessorLootPool poolAccess) { getPotentialItems(registries, poolAccess.bookshelf$entries(), consumer); } } } } /** * Gets potential drops for a list of loot pool entries. * * @param registries The current registries. * @param entries A list of entries to examine. * @param collector Collects entries in your desired format. */ public static void getPotentialItems(@NotNull RegistryAccess registries, List entries, Consumer collector) { for (LootPoolEntryContainer entry : entries) { getPotentialItems(registries, entry, collector); } } /** * Gets potential drops from a loot pool entry. * * @param registries The current registries. * @param entry The pool entry to examine. * @param collector Collects entries in your desired format. */ public static void getPotentialItems(@NotNull RegistryAccess registries, LootPoolEntryContainer entry, Consumer collector) { bootstrap(); final LootPoolEntryDescriber describer = DESCRIBERS.get(entry.getType()); if (describer != null) { describer.getPotentialDrops(registries, entry, collector); } else { collector.accept(UNKNOWN_ITEM_DISPLAY.get()); } } /** * Adds an ItemStack to a list, only if the item does not stack with any of the items already in the list. * * @param items The list to add to. * @param toAdd The entry to add. */ private static void addStacking(List items, ItemStack toAdd) { for (ItemStack existing : items) { if (Objects.equals(existing, toAdd) || ItemStack.isSameItemSameComponents(existing, toAdd)) { return; } } items.add(toAdd); } private static void getTagItems(TagKey tag, Consumer collector) { for (Holder item : BuiltInRegistries.ITEM.getTagOrEmpty(tag)) { collector.accept(new ItemStack(item)); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/menu/data/BlockPosData.java ================================================ package net.darkhax.bookshelf.common.api.menu.data; import net.minecraft.core.BlockPos; import net.minecraft.world.inventory.ContainerData; import java.util.Arrays; /** * Allows a block position to be kept in sync using the container system. The server should always construct this * directly while the client should use SimpleContainerData with size of 3. */ public class BlockPosData implements ContainerData { private final int[] pos; private final boolean mutable; public BlockPosData(BlockPos pos) { this(pos, false); } public BlockPosData(BlockPos pos, boolean mutable) { this.pos = new int[]{pos.getX(), pos.getY(), pos.getZ()}; this.mutable = mutable; } @Override public int get(int slot) { return pos[slot]; } @Override public void set(int slot, int value) { if (this.mutable) { pos[slot] = value; } } @Override public int getCount() { return 3; } /** * Gets the BlockPos currently held by the container data. This should only be called on the server. * * @return The BlockPos being held. */ public BlockPos getPos() { return new BlockPos(pos[0], pos[1], pos[2]); } /** * Reads a BlockPos from untyped container data. This should be used to read the position on the client which should * be using a SimpleContainerData and not a BlockPosData. * * @param data The data to read from. * @return The BlockPos that was ready. */ public static BlockPos readPos(ContainerData data) { if (data.getCount() != 3) { throw new IllegalStateException("Can not read BlockPos from container data. Expected 3 values, found " + data.getCount() + ". data=" + Arrays.toString(toArray(data))); } return new BlockPos(data.get(0), data.get(1), data.get(2)); } private static int[] toArray(ContainerData containerData) { final int[] data = new int[containerData.getCount()]; for (int i = 0; i < containerData.getCount(); i++) { data[i] = containerData.get(i); } return data; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/menu/slot/InputSlot.java ================================================ package net.darkhax.bookshelf.common.api.menu.slot; import com.mojang.datafixers.util.Pair; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.Container; import net.minecraft.world.inventory.InventoryMenu; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Predicate; /** * A basic input slot implementation. */ public class InputSlot extends Slot { private final ResourceLocation emptyTexture; private final Predicate canPlace; public InputSlot(Container container, int slot, int x, int y, ResourceLocation emptyTexture) { this(container, slot, x, y, emptyTexture, stack -> true); } public InputSlot(Container container, int slot, int x, int y, ResourceLocation emptyTexture, Predicate canPlace) { super(container, slot, x, y); this.emptyTexture = emptyTexture; this.canPlace = canPlace; } @Override public int getMaxStackSize() { return 1; } @Nullable @Override public Pair getNoItemIcon() { return Pair.of(InventoryMenu.BLOCK_ATLAS, this.emptyTexture); } @Override public boolean mayPlace(@NotNull ItemStack stack) { return this.canPlace.test(stack); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/menu/slot/OutputSlot.java ================================================ package net.darkhax.bookshelf.common.api.menu.slot; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.BiConsumer; /** * A basic output slot implementation. */ public class OutputSlot extends Slot { @Nullable private final BiConsumer takeFunc; public OutputSlot(Container potContainer, int slot, int x, int y) { this(potContainer, slot, x, y, null); } public OutputSlot(Container potContainer, int slot, int x, int y, @Nullable BiConsumer takeFunc) { super(potContainer, slot, x, y); this.takeFunc = takeFunc; } @Override public boolean mayPlace(@NotNull ItemStack stack) { return false; } @Override public void onTake(@NotNull Player player, @NotNull ItemStack stack) { super.onTake(player, stack); if (this.takeFunc != null) { this.takeFunc.accept(player, stack); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/network/AbstractPacket.java ================================================ package net.darkhax.bookshelf.common.api.network; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; /** * A basic packet implementation. * * @param The type of the payload. */ public abstract class AbstractPacket implements IPacket { /** * The type of the payload. */ private final CustomPacketPayload.Type type; /** * A codec to serialize the payload. */ private final StreamCodec codec; /** * The intended destination of the payload. */ private final Destination direction; /** * A packet that is sent from the server to the client. * * @param id The packet ID. * @param codec The payload codec. */ public AbstractPacket(ResourceLocation id, StreamCodec codec) { this(id, codec, Destination.SERVER_TO_CLIENT); } /** * A simple packet type. * * @param id The packet ID. * @param codec The payload codec. * @param direction The intended destination of the packet. */ public AbstractPacket(ResourceLocation id, StreamCodec codec, Destination direction) { this.type = new CustomPacketPayload.Type<>(id); this.codec = codec; this.direction = direction; } @Override public CustomPacketPayload.Type type() { return this.type; } @Override public StreamCodec streamCodec() { return this.codec; } @Override public Destination destination() { return this.direction; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/network/Destination.java ================================================ package net.darkhax.bookshelf.common.api.network; /** * Determines where the packet will be resolved. */ public enum Destination { /** * Describes a situation where the server has a payload that will be sent to a client. The payload will be handled * on the client and can use code that is only available on a dedicated client. */ SERVER_TO_CLIENT, /** * Describes a situation where the client has a payload that will be sent to a server. The payload will be handled * by the server and can access the game state. Please keep in mind that payloads originating on the client can be * forged and should not be trusted without an appropriate level of validation on the server. */ CLIENT_TO_SERVER, /** * Describes a situation where the packet can originate from and be handled by a client or a server. These packets * have the limitations of both SERVER_TOL_CLIENT and CLIENT_TO_SERVER packets. */ BIDIRECTIONAL; /** * Checks if the payload can be handled on a server. * * @return If the payload can be handled by a server. */ public boolean handledByServer() { return this == CLIENT_TO_SERVER || this == BIDIRECTIONAL; } /** * Checks if the payload can be handled on a client. * * @return If the payload can be handled by a client. */ public boolean handledByClient() { return this == SERVER_TO_CLIENT || this == BIDIRECTIONAL; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/network/INetworkHandler.java ================================================ package net.darkhax.bookshelf.common.api.network; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; /** * Provides platform specific implementations of network related code. Access using * {@link net.darkhax.bookshelf.common.api.service.Services#NETWORK}. */ public interface INetworkHandler { /** * Registers a Bookshelf packet type to the packet registry. * * @param packet The packet type to register. * @param The type of the payload. */ void register(IPacket packet); /** * Sends a payload from the client to the server. * * @param payload The payload to send. * @param The type of the payload. */ void sendToServer(T payload); /** * Sends a packet from the server to a player. * * @param recipient The recipient of the packet. * @param payload The payload to send. * @param The type of the payload. */ void sendToPlayer(ServerPlayer recipient, T payload); /** * Tests if a payload type can be sent to a player. * * @param recipient The recipient of the packet. * @param payload The payload to test. * @return If the payload can be sent to the recipient player. */ default boolean canSendPacket(ServerPlayer recipient, CustomPacketPayload payload) { return this.canSendPacket(recipient, payload.type().id()); } /** * Tests if a payload type can be sent to a player. * * @param recipient The recipient of the packet. * @param packet The packet to test. * @return If the payload can be sent to the recipient player. */ default boolean canSendPacket(ServerPlayer recipient, IPacket packet) { return this.canSendPacket(recipient, packet.type().id()); } /** * Tests if a payload type can be sent to a player. * * @param recipient The recipient of the packet. * @param payloadId The payload type ID. * @return If the payload can be sent to the recipient player. */ boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/network/IPacket.java ================================================ package net.darkhax.bookshelf.common.api.network; import net.darkhax.bookshelf.common.api.PhysicalSide; import net.darkhax.bookshelf.common.api.annotation.OnlyFor; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.client.Minecraft; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; import org.jetbrains.annotations.Nullable; /** * Defines a custom payload packet. These packets must be registered using an * {@link net.darkhax.bookshelf.common.api.registry.ContentProvider}. * * @param The type of the payload. */ public interface IPacket { /** * Gets the payload type. * * @return The payload type. */ CustomPacketPayload.Type type(); /** * Gets a stream codec that can serialize the payload across the network. * * @return The stream coded used to serialize the payload. */ StreamCodec streamCodec(); /** * Defines how the packet is meant to be sent and where it should be handled. * * @return The intended destination of the packet. */ Destination destination(); /** * This method will be called when the custom payload is received. This method can be called on both the client and * server, depending on the destination type defined by {@link #destination()}. * * @param sender The sender of the packet. This will always be null for packets handled on the client. * @param isServer True when the packet is being handled on the server. * @param payload The payload that was received. */ void handle(@Nullable ServerPlayer sender, boolean isServer, T payload); /** * Sends the packet from the server to a specific player. * * @param recipient The intended recipient of the payload. * @param payload The payload to send. */ default void toPlayer(ServerPlayer recipient, T payload) { if (!this.destination().handledByClient()) { Constants.LOG.error("Attempted to send invalid packet {} to client! Class: {} Destination: {} Payload: {}", this.type().id(), this.getClass(), this.destination(), payload.toString()); throw new IllegalStateException("Attempted to send invalid packet " + this.type().id() + " to client!"); } Services.NETWORK.sendToPlayer(recipient, payload); } /** * Sends the packet from the server to all connected players. * * @param level A serverside level, used to access the player list. * @param payload The payload to send. */ default void toAllPlayers(ServerLevel level, T payload) { toAllPlayers(level.getServer(), payload); } /** * Sends the packet from the server to all connected players. * * @param server The server instance, used to access the player list. * @param payload The payload to send. */ default void toAllPlayers(MinecraftServer server, T payload) { toAllPlayers(server.getPlayerList(), payload); } /** * Sends the packet from the server to all connected players. * * @param playerList The player list. * @param payload The payload to send. */ default void toAllPlayers(PlayerList playerList, T payload) { for (ServerPlayer player : playerList.getPlayers()) { toPlayer(player, payload); } } /** * Sends a packet from a client to the server. * * @param payload The payload to send. */ @OnlyFor(PhysicalSide.CLIENT) default void toServer(T payload) { if (!this.destination().handledByServer()) { Constants.LOG.error("Attempted to send invalid packet {} to server! Class: {} Destination: {} Payload: {}", this.type().id(), this.getClass(), this.destination(), payload.toString()); throw new IllegalStateException("Attempted to send invalid packet " + this.type().id() + " to server!"); } if (Minecraft.getInstance().getConnection() == null) { Constants.LOG.error("Attempted to send packet {} before a connection to a server has been established!", this.type().id()); throw new IllegalStateException("Attempted to send packet " + this.type().id() + " before being connected to a server!"); } Services.NETWORK.sendToServer(payload); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/ContentProvider.java ================================================ package net.darkhax.bookshelf.common.api.registry; import com.mojang.brigadier.CommandDispatcher; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.PhysicalSide; import net.darkhax.bookshelf.common.api.annotation.OnlyFor; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootPoolAdditionAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter; import net.minecraft.advancements.CriterionTrigger; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.core.component.DataComponentType; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.animal.CatVariant; import net.minecraft.world.item.Item; import net.minecraft.world.item.alchemy.Potion; import net.minecraft.world.item.alchemy.PotionBrewing; import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.storage.loot.functions.LootItemFunctionType; import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; /** * An interface for adding custom game content such as blocks and items during the appropriate stages of the game's * lifecycle. *

* Implementations of this interface are discovered automatically by Bookshelf using the {@link java.util.ServiceLoader} * mechanism. To make your provider loadable, add the fully qualified name of your implementation to the file: *

 *     META-INF/services/net.darkhax.bookshelf.common.api.registry.ContentProvider
 * 
*/ public interface ContentProvider { /** * Registers new attributes with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineAttributes(GameRegistryAdapter registry) { } /** * Registers new blocks with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineBlocks(BlockRegistryAdapter registry) { } /** * Registers new block entities with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineBlockEntities(GameRegistryAdapter> registry) { } /** * Registers new items with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineItems(GameRegistryAdapter registry) { } /** * Registers new recipe types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineRecipeTypes(RecipeTypeAdapter registry) { } /** * Registers new creative mode tabs with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineCreativeTabs(CreativeModeTabAdapter registry) { } /** * Registers new command argument types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineCommandArguments(CommandArgumentAdapter registry) { } /** * Registers new commands with the game. This may happen multiple times per game instance depending on user * actions. * * @param dispatcher The command dispatcher to populate with your new commands. * @param context Context used to build commands. * @param selection The type of commands that should be registered. */ default void defineCommands(CommandDispatcher dispatcher, CommandBuildContext context, Commands.CommandSelection selection) { } /** * Registers new ingredient types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineIngredientTypes(IngredientTypeAdapter registry) { } /** * Adds new trades to the villager trade pools. * * @param registry Adapts registry requests to the current mod loader. */ default void defineTrades(VillagerTradeAdapter registry) { } /** * Registers new mob effects with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineMobEffects(GameRegistryAdapter registry) { } /** * Registers new criteria triggers with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineCriteriaTriggers(GameRegistryAdapter> registry) { } /** * Registers an item predicate type with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineItemSubPredicates(GameRegistryAdapter> registry) { } /** * Registers entity types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineEntities(GameRegistryAdapter> registry) { } /** * Registers cat variants with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineCatVariants(GameRegistryAdapter registry) { } /** * Registers potions with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void definePotions(GameRegistryAdapter registry) { } /** * Registers new potion brewing recipes with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineBrews(PotionBrewing.Builder registry) { } /** * Registers new decorated pot patterns with the game, and create associations between items and patterns. * * @param registry Adapts registry requests to the current mod loader. */ default void definePotPatterns(PotPatternAdapter registry) { } /** * Registers new item components with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineItemComponents(GameRegistryAdapter> registry) { } /** * Registers new enchantment components with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineEnchantmentComponents(GameRegistryAdapter> registry) { } /** * Registers new loot conditions with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineLootConditions(GameRegistryAdapter registry) { } /** * Registers new loot functions with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineLootFunctions(GameRegistryAdapter> registry) { } /** * Registers new recipe serializers with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineRecipeSerializers(GameRegistryAdapter> registry) { } /** * Registers new loot entry types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineLootEntryTypes(LootEntryTypeAdapter registry) { } /** * Inject entries into existing loot pools. For example, this can be used to add new loot to the dungeon loot * chest. * * @param registry Adapts registry requests to the current mod loader. */ default void defineLootPoolAdditions(LootPoolAdditionAdapter registry) { } /** * Registers a new descriptor for loot entries. * * @param registry Accepts registry requests. */ default void defineLootDescriptions(LootDescriptionAdapter registry) { } /** * Registers a new bookshelf load condition for JSON resources. * * @param registry Accepts registry requests. */ default void defineLoadConditions(GenericRegistryAdapter> registry) { } /** * Registers new menu types with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineMenuType(MenuTypeAdapter registry) { } /** * Registers new packets with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void definePackets(PacketAdapter registry) { } /** * Registers new sound events with the game. * * @param registry Adapts registry requests to the current mod loader. */ default void defineSounds(SoundEventAdapter registry) { } /** * Associates menu types with screens. * * @param registry Adapts registry requests to the current mod loader. */ @OnlyFor(PhysicalSide.CLIENT) default void defineMenuScreens(MenuScreenAdapter registry) { } /** * Associates blocks with a render type. For now, NeoForge also requires this to be defined in the model file. * * @param registry Adapts registry requests to the current mod loader. */ @OnlyFor(PhysicalSide.CLIENT) default void defineBlockRenderTypes(BlockRenderTypeAdapter registry) { } /** * Associates a block entity with a block entity renderer. * * @param registry Adapts registry requests to the current mod loader. */ @OnlyFor(PhysicalSide.CLIENT) default void defineBlockRenderers(BlockEntityRendererAdapter registry) { } /** * Gets the namespace that all content from the provider should be registered under. This MUST be the same modid * that is used by your NeoForge/Fabric mod. * * @return The namespace to register content with. */ String namespace(); /** * Checks if content from the provider should be loaded or not. All providers will be loaded by default, however * custom implementations may have additional requirements. *

* Bookshelf will still classload your provider if this returns false, this only prevents content from being loaded. * It is the implementers responsibility to ensure their class can be classloaded safely. * * @return If content from this provider should be loaded. */ default boolean canLoad() { return true; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/RegistrationContext.java ================================================ package net.darkhax.bookshelf.common.api.registry; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.DecoratedPotPattern; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * Holds context that is shared between different registry adapters. */ public final class RegistrationContext { private final String namespace; private final Map, Block>, Function> placeableBlocks = new HashMap<>(); private static final Map> INTERNAL_POT_PATTERN_ITEMS = new HashMap<>(); public static final Map> POT_PATTERN_ITEMS = Collections.unmodifiableMap(INTERNAL_POT_PATTERN_ITEMS); public RegistrationContext(String namespace) { this.namespace = namespace; } /** * Gets the namespace that all new content should be registered with. * * @return The namespace new content is registered with. */ public String namespace() { return this.namespace; } /** * Associates a block with a factory that provides its corresponding item form. The produced item will be * automatically registered with the same ID as the block. * * @param block The block to associate the item with. * @param itemBlock A factory that creates the placer item. */ public void addPlaceableBlock(RegistryReference, Block> block, Function itemBlock) { this.placeableBlocks.put(block, itemBlock); } /** * Provides an unmodifiable view of placeable blocks and their associated item factories. * * @return An unmodifiable map of placeable blocks to their placer item factories. */ public Map, Block>, Function> getPlaceableBlocks() { return Collections.unmodifiableMap(this.placeableBlocks); } /** * Associates an item with a decorated pot pattern. Replacing existing associations is not a supported use case. * * @param item The item to associate with the pattern. * @param pattern The pattern displayed by the item. */ public void addPotPatternItem(Item item, ResourceKey pattern) { if (INTERNAL_POT_PATTERN_ITEMS.containsKey(item)) { Constants.LOG.warn("Mod {} has changed the pot pattern of {} to {} from {}.", this.namespace(), BuiltInRegistries.ITEM.getKey(item), pattern.location(), INTERNAL_POT_PATTERN_ITEMS.get(item).location()); } INTERNAL_POT_PATTERN_ITEMS.put(item, pattern); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/RegistryReference.java ================================================ package net.darkhax.bookshelf.common.api.registry; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; /** * Represents an entry in a game registry. * * @param key The key the value was registered with. * @param value A supplier that produces the registered value. * @param The type of the registry key. * @param The type of the registered value. */ public record RegistryReference(K key, CachedSupplier value) { /** * A helper method that produces a reference for a registry that uses ResourceLocation based keys. * * @param key The key the value was registered with. * @param value A supplier that produces the registered value. * @param The type of the registered value. * @return A reference to the registry entry. */ public static RegistryReference location(ResourceLocation key, CachedSupplier value) { return new RegistryReference<>(key, value); } /** * A helper method that produces a reference for a registry that uses ResourceKey. * * @param key The key to lookup. * @param The type of the value held in the registry. * @return A reference to a value in a registry. */ public static RegistryReference, V> resource(ResourceKey key) { return new RegistryReference<>(key, CachedSupplier.of(key)); } /** * A helper method that produces a reference for a registry that uses ResourceKey. * * @param registryKey The key for the registry the value is registered in. * @param key The key the value was registered with. * @param value A supplier that produces the registered value. * @param The type of the registered value. * @return A reference to the registry entry. */ public static RegistryReference, V> resource(ResourceKey> registryKey, ResourceLocation key, CachedSupplier value) { return new RegistryReference<>(ResourceKey.create(registryKey, key), value); } /** * A helper method that produces a reference for a registry that uses ResourceKey. * * @param registry The registry the value is registered in. * @param key The key the value was registered with. * @param value A supplier that produces the registered value. * @param The type of the registered value. * @return A reference to the registry entry. */ public static RegistryReference, V> resource(Registry registry, ResourceLocation key, CachedSupplier value) { return resource(registry.key(), key, value); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/GameRegistryAdapter.java ================================================ package net.darkhax.bookshelf.common.api.registry.adapters; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import java.util.function.BiConsumer; import java.util.function.Supplier; /** * A basic registry adapter that can register into most vanilla style registries. * * @param The type of value held by the registry. */ public class GameRegistryAdapter implements RegistryAdapter, V> { /** * Context that is shared by all registry adapters owned by the same namespace. */ protected final RegistrationContext context; /** * The id of the registry being adapted. */ protected final ResourceKey> registryKey; /** * A function that accepts and registers a key and value supplier. */ protected final BiConsumer, Supplier> registryFunc; public GameRegistryAdapter(RegistrationContext context, ResourceKey> registryKey, BiConsumer, Supplier> registryFunc) { this.context = context; this.registryKey = registryKey; this.registryFunc = registryFunc; } @Override public RegistryReference, V> add(String key, Supplier value) { final ResourceKey resourceKey = ResourceKey.create(registryKey, ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key)); this.registryFunc.accept(resourceKey, value); return RegistryReference.resource(resourceKey); } /** * Creates a new ResourceLocation using the current namespace. * * @param key The path of the ResourceLocation. * @return A new ResourceLocation with the current namespace. */ public final ResourceLocation id(String key) { return ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/GenericRegistryAdapter.java ================================================ package net.darkhax.bookshelf.common.api.registry.adapters; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.minecraft.resources.ResourceLocation; import java.util.function.BiConsumer; import java.util.function.Supplier; /** * A basic registry adapter that can register into registries that are not standard vanilla registries. * * @param The type of value held by the registry. */ public class GenericRegistryAdapter implements RegistryAdapter { /** * Context that is shared by all registry adapters owned by the same namespace. */ protected final RegistrationContext context; /** * A function that accepts and registers a key and value supplier. */ protected final BiConsumer> registryFunc; public GenericRegistryAdapter(RegistrationContext context, BiConsumer> registryFunc) { this.context = context; this.registryFunc = registryFunc; } /** * Adds a value to the registry. Values are not necessarily registered immediately. * * @param id The ID to register the value under. * @param value A supplier that produces the value to register. * @return A reference to the registry entry. */ public RegistryReference add(ResourceLocation id, Supplier value) { final CachedSupplier cache = CachedSupplier.cache(value); this.registryFunc.accept(id, cache); return RegistryReference.location(id, cache); } /** * Adds a value to the registry. Values are not necessarily registered immediately. * * @param id The ID to register the value under. * @param value The value to register. * @return A reference to the registry entry. */ public RegistryReference add(ResourceLocation id, V value) { return this.add(id, () -> value); } @Override public RegistryReference add(String key, Supplier value) { return this.add(ResourceLocation.fromNamespaceAndPath(this.context.namespace(), key), value); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/registry/adapters/RegistryAdapter.java ================================================ package net.darkhax.bookshelf.common.api.registry.adapters; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import java.util.function.Supplier; /** * Provides a loader agnostic interface for registering content. * * @param The type of key used by the registry. * @param The type of value being registered. */ public interface RegistryAdapter { /** * Adds a value to the registry. Values are not necessarily registered immediately. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param value The value to register. * @return A reference to the registry entry. */ default RegistryReference add(String key, V value) { return this.add(key, () -> value); } /** * Adds a value to the registry. Values are not necessarily registered immediately. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param value A supplier that produces the value to register. * @return A reference to the registry entry. */ RegistryReference add(String key, Supplier value); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/service/Services.java ================================================ package net.darkhax.bookshelf.common.api.service; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.network.INetworkHandler; import net.darkhax.bookshelf.common.api.registry.ContentProvider; import net.darkhax.bookshelf.common.api.util.IGameplayHelper; import net.darkhax.bookshelf.common.api.util.IPlatformHelper; import net.darkhax.bookshelf.common.impl.Constants; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.ServiceLoader; import java.util.stream.Collectors; public class Services { public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class); public static final CachedSupplier> CONTENT = CachedSupplier.cache(() -> loadMany(ContentProvider.class)); public static final IGameplayHelper GAMEPLAY = load(IGameplayHelper.class); public static final INetworkHandler NETWORK = load(INetworkHandler.class); public static T load(Class clazz) { final T loadedService = ServiceLoader.load(clazz).findFirst().orElseThrow(() -> new NullPointerException("Failed to load service for " + clazz.getName())); Constants.LOG.debug("Loaded {} for service {}.", loadedService, clazz); return loadedService; } public static List loadMany(Class clazz) { final List entries = ServiceLoader.load(clazz).stream().map(ServiceLoader.Provider::get).toList(); Constants.LOG.debug("Loaded {} entries for {}. {}", entries.size(), clazz, entries.stream().map(entry -> entry.getClass().getCanonicalName()).collect(Collectors.joining())); return entries; } /** * Finds implementations of a service without initializing or classloading them. * * @param name The fully qualified name of the service. * @return A list of all implementations that were found. * @throws IOException Sometimes stuff can't be read. */ public static List findServices(String name) throws IOException { final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); final List matches = new ArrayList<>(); final Enumeration candidates = classLoader.getResources("META-INF/services/" + name); while (candidates.hasMoreElements()) { try (InputStream input = candidates.nextElement().openStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (!line.startsWith("#") && !line.isEmpty()) { matches.add(line); } } } } return matches; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/font/BuiltinFonts.java ================================================ package net.darkhax.bookshelf.common.api.text.font; import net.minecraft.resources.ResourceLocation; import java.util.Set; /** * Constant references to all built-in Minecraft fonts. These fonts are included in the original game and are not * bundled or provided by Bookshelf. */ public enum BuiltinFonts implements IFontEntry { /** * The default pixel font that appears in the game. */ DEFAULT("default"), /** * A magical font based on the Standard Galactic Alphabet. This font is used by the enchanting table and is * associated with the enchantment system. */ ALT("alt"), /** * A rune font that is used by the Illagers in Minecraft Dungeons. */ ILLAGER("illageralt"), /** * A plain font that is not stylized. */ UNIFORM("uniform"); public static final Set FONT_IDS = Set.of(DEFAULT.fontId, ALT.fontId, ILLAGER.fontId, UNIFORM.fontId); private final ResourceLocation fontId; BuiltinFonts(String path) { this(ResourceLocation.tryBuild("minecraft", path)); } BuiltinFonts(ResourceLocation fontID) { this.fontId = fontID; } @Override public ResourceLocation identifier() { return this.fontId; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/font/IFontEntry.java ================================================ package net.darkhax.bookshelf.common.api.text.font; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; public interface IFontEntry { /** * Gets the ID of the font. * * @return The font ID. */ ResourceLocation identifier(); /** * Gets the localized name of the font. * * @return The localized name of the font. */ default MutableComponent displayName() { return TextHelper.fromResourceLocation("font", null, this.identifier()); } /** * Gets a description of the font. * * @return A description of the font. */ default MutableComponent description() { return TextHelper.fromResourceLocation("font", "desc", this.identifier()); } /** * Gets some text that can be used as a preview for the font in-game. * * @return The preview text for the font. */ default MutableComponent preview() { return TextHelper.fromResourceLocation("font", "preview", this.identifier()); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/format/IPropertyFormat.java ================================================ package net.darkhax.bookshelf.common.api.text.format; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; public interface IPropertyFormat { /** * A namespaced identifier that is used to derive localization keys for the format. * * @return The namespace ID for the format. */ ResourceLocation formatKey(); /** * Formats a property and value using the alignment. * * @param property The name of the property. * @param value The value of the property. * @return A component that represents an aligned property and value. */ default MutableComponent format(Component property, Component value) { return TextHelper.fromResourceLocation("format", null, this.formatKey(), property, value); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/format/PropertyFormat.java ================================================ package net.darkhax.bookshelf.common.api.text.format; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; /** * Formats a property string using various separator patterns. */ public enum PropertyFormat implements IPropertyFormat { /** * Formats a property with the separator aligned to the right. Example: "property: value". */ RIGHT("right"), /** * Formats a property with the separator aligned in the center. Example: "property : value". */ CENTER("center"), /** * Formats a property with the separator aligned to the left. Example: "property :value". */ LEFT("left"), /** * Formats a property using a single space as the separator. Example: "property value". */ SPACED("spaced"), /** * Formats a property without a separator. Example: "propertyvalue". */ NONE("none"); private final ResourceLocation formatKey; PropertyFormat(String key) { this.formatKey = Constants.id(key); } @Override public ResourceLocation formatKey() { return this.formatKey; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/unit/IUnit.java ================================================ package net.darkhax.bookshelf.common.api.text.unit; import net.darkhax.bookshelf.common.api.text.format.PropertyFormat; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; public interface IUnit { /** * A namespaced identifier that is used to derive localization keys for the unit. * * @return The namespace ID for the unit. */ ResourceLocation unitKey(); /** * Gets the name of the unit. * * @return The name of the unit. */ default MutableComponent unitName() { return Component.translatable("units." + this.unitKey().getNamespace() + "." + this.unitKey().getPath()); } /** * Gets the plural name of the unit. * * @return The plural name of the unit. */ default MutableComponent plural() { return Component.translatable("units.bookshelf." + this.unitKey().getNamespace() + "." + this.unitKey() + ".plural"); } /** * Gets the abbreviated name of the unit. * * @return The abbreviated name of the unit. */ default MutableComponent abbreviated() { return Component.translatable("units." + this.unitKey().getNamespace() + "." + this.unitKey() + ".abbreviated"); } /** * Formats an amount of the unit as a text component. * * @param amount The amount of the unit. * @return The formatted text. If the amount is plural the plural name will be used. */ default MutableComponent format(int amount) { return format(amount, PropertyFormat.LEFT); } /** * Formats an amount of the unit as a text component. * * @param amount The amount of the unit. * @param format The property format to use when displaying the unit and value. * @return The formatted text. If the amount is plural the plural name will be used instead. */ default MutableComponent format(int amount, PropertyFormat format) { return format.format((amount == 1 || amount == -1) ? this.unitName() : this.plural(), Component.literal(Integer.toString(amount))); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/text/unit/Units.java ================================================ package net.darkhax.bookshelf.common.api.text.unit; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; /** * Represents various units that can be displayed in game. */ public enum Units implements IUnit { TICK("tick"), NANOSECOND("nanosecond"), MILLISECOND("millisecond"), SECOND("second"), MINUTE("minute"), HOUR("hour"), DAY("day"), WEEK("week"), MONTH("month"), YEAR("year"); private final ResourceLocation key; Units(String key) { this.key = Constants.id(key); } @Override public ResourceLocation unitKey() { return this.key; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/CommandHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.darkhax.bookshelf.common.api.commands.IEnumCommand; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.world.entity.Entity; import java.util.function.Supplier; public class CommandHelper { /** * Creates a command with branching paths that represent the values of an enum. * * @param parent The name of the root parent command node. * @param enumClass The enum class to use. * @param The type of the enum. * @return The newly created command node. */ public static & IEnumCommand> LiteralArgumentBuilder buildFromEnum(String parent, Class enumClass) { final LiteralArgumentBuilder parentNode = LiteralArgumentBuilder.literal(parent); parentNode.requires(getLowestLevel(enumClass)); buildFromEnum(parentNode, enumClass); return parentNode; } /** * Creates branching command paths that represent the values of an enum. * * @param parent The parent node to branch off from. * @param enumClass The enum class to use. * @param The type of the enum. */ public static & IEnumCommand> void buildFromEnum(ArgumentBuilder parent, Class enumClass) { if (!enumClass.isEnum()) { throw new IllegalStateException("Class '" + enumClass.getCanonicalName() + "' is not an enum!"); } for (T enumEntry : enumClass.getEnumConstants()) { final LiteralArgumentBuilder literal = LiteralArgumentBuilder.literal(enumEntry.getCommandName()); literal.requires(enumEntry.requiredPermissionLevel()).executes(enumEntry); parent.then(literal); } } /** * Gets the lowest required permission level for an enum command. * * @param enumClass The enum class to use. * @param The type of the enum. * @return The lowest required permission level for an enum command. */ public static & IEnumCommand> PermissionLevel getLowestLevel(Class enumClass) { if (!enumClass.isEnum()) { throw new IllegalStateException("Class '" + enumClass.getCanonicalName() + "' is not an enum!"); } PermissionLevel level = PermissionLevel.OWNER; for (T enumEntry : enumClass.getEnumConstants()) { if (enumEntry.requiredPermissionLevel().get() < level.get()) { level = enumEntry.requiredPermissionLevel(); } } return level; } /** * @deprecated This only works on Fabric. */ @Deprecated public static boolean hasArgument(String argument, CommandContext context) { return hasArgument(argument, context, Object.class); } public static boolean hasArgument(String argument, CommandContext context, Class argType) { try { return context.getArgument(argument, argType) != null; } catch (Exception e) { return false; } } public static T getArgument(String argument, CommandContext context, Class argType, Supplier fallback) { return hasArgument(argument, context, argType) ? context.getArgument(argument, argType) : fallback.get(); } public static Entity getEntity(String argName, CommandContext ctx, Supplier fallback) throws CommandSyntaxException { return CommandHelper.hasArgument(argName, ctx, EntitySelector.class) ? EntityArgument.getEntity(ctx, argName) : fallback.get(); } public static Entity getEntityOrSender(String argName, CommandContext ctx) throws CommandSyntaxException { return CommandHelper.hasArgument(argName, ctx, EntitySelector.class) ? EntityArgument.getEntity(ctx, argName) : ctx.getSource().getEntity(); } public static boolean getBooleanArg(String argName, CommandContext ctx, Supplier fallback) { return CommandHelper.hasArgument(argName, ctx, Boolean.class) ? BoolArgumentType.getBool(ctx, argName) : fallback.get(); } public static boolean getBooleanArg(String argName, CommandContext ctx) { return getBooleanArg(argName, ctx, () -> false); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/DataHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import com.mojang.serialization.MapCodec; import io.netty.buffer.ByteBuf; import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; import net.minecraft.tags.TagKey; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeSerializer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.function.Predicate; public class DataHelper { public static HolderSet getTagOrEmpty(@Nullable HolderLookup.Provider provider, ResourceKey> registryKey, TagKey tag) { if (provider != null) { final Optional> optional = provider.lookupOrThrow(registryKey).get(tag); if (optional.isPresent()) { return optional.get(); } } return HolderSet.direct(); } /** * Creates a sublist of a ListTag. Unlike {@link java.util.AbstractList#subList(int, int)}, the sublist is a new * list instance and changes to the original will not be propagated. * * @param list The list to create a sublist from. * @param from The starting index. * @param to The ending index. * @return A sublist created from the input list. */ public static ListTag subList(ListTag list, int from, int to) { if (list == null) { throw new IllegalStateException("The input list must not be null!"); } if (from < 0 || to > list.size() || from > to) { throw new IndexOutOfBoundsException("Invalid range! from=" + from + " to=" + to + " size=" + list.size()); } final ListTag subList = new ListTag(); for (int i = from; i < to; i++) { subList.add(list.get(i)); } return subList; } /** * Creates a sublist of an inventory tag based on a predicate on the slot indexes. * * @param list The inventory list tag. * @param slots A predicate for which item slots should be included in the sublist. * @return A sublist created from the input list. */ public static ListTag containerSubList(ListTag list, Predicate slots) { if (list == null) { throw new IllegalStateException("The input list must not be null!"); } final ListTag subList = new ListTag(); for (Tag tag : list) { if (tag instanceof CompoundTag entry && entry.contains("Slot", Tag.TAG_BYTE) && slots.test(entry.getInt("Slot"))) { subList.add(tag); } } return subList; } /** * Creates a new stream codec for an optional value. * * @param streamCodec A codec that can serialize the content type. * @param The type of the byte buffer. * @param The content type of the stream. * @return An optional stream codec. */ public static StreamCodec> optionalStream(StreamCodec streamCodec) { return StreamCodec.of( (buf, val) -> { buf.writeBoolean(val.isPresent()); val.ifPresent(v -> streamCodec.encode(buf, v)); }, buf -> { if (buf.readBoolean()) { final V val = streamCodec.decode(buf); return Optional.of(val); } return Optional.empty(); }); } /** * Creates a new recipe serializer. * * @param codec A codec for JSON/NBT data. * @param stream A codec for networking. * @param The type of the recipe. * @return A recipe serializer object. */ public static > RecipeSerializer recipeSerializer(MapCodec codec, StreamCodec stream) { return new RecipeSerializer<>() { @NotNull @Override public MapCodec codec() { return codec; } @NotNull @Override public StreamCodec streamCodec() { return stream; } }; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/ExperienceHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.minecraft.world.entity.player.Player; public final class ExperienceHelper { /** * Attempts to charge the player an experience point cost. If the player can not afford the full amount they will * not be charged and false will be returned. * * @param player The player to charge. * @param cost The amount to charge the player in experience points. * @return True if the amount was paid. */ public static boolean chargeExperiencePoints(Player player, int cost) { final int playerExperience = getExperiencePoints(player); if (playerExperience >= cost) { player.giveExperiencePoints(-cost); // The underlying EXP system uses a float which is prone to rounding errors. This will sometimes leave // players with a small fraction of exp progress that is worth less than 1 exp point. These rounding errors // are so small that they do not introduce functionality issues however they can trigger a vanilla bug // where the EXP bar will still render a few pixels of progress even when the player has no exp points. To // prevent this issue here we simply reset all progress when the player spends all of their points. if (getExperiencePoints(player) <= 0) { player.experienceProgress = 0f; } return true; } return false; } /** * Calculates the amount of experience points the player currently has. This should be used in favour of * {@link Player#totalExperience} which deceptively does not track the amount of experience the player currently * has. *

* Contrary to popular belief the {@link Player#totalExperience} value actually loosely represents how much * experience points the player has earned during their current life. This value is akin to the old player score * metric and appears to be predominantly legacy code. Relying on this value is often incorrect as negative changes * to the player level such as enchanting, the anvil, and the level command will not reduce this value. * * @param player The player to calculate the total experience points of. * @return The amount of experience points held by the player. */ public static int getExperiencePoints(Player player) { // Start by calculating how many EXP points the player's current level is worth. int exp = getTotalPointsForLevel(player.experienceLevel); // Add the amount of experience points the player has earned towards their next level. exp += player.experienceProgress * getTotalPointsForLevel(player.experienceLevel + 1); return exp; } /** * Calculates the amount of additional experience points required to reach the given level when starting from the * previous level. This will also be the amount of experience points that an individual level is worth. * * @param level The level to calculate the point step for. * @return The amount of points required to reach the given level when starting from the previous level. */ public static int getPointForLevel(int level) { if (level == 0) { return 0; } else if (level > 30) { return 112 + (level - 31) * 9; } else if (level > 15) { return 37 + (level - 16) * 5; } else { return 7 + (level - 1) * 2; } } /** * Calculates the amount of additional experience points required to reach the target level when starting from the * starting level. * * @param startingLevel The level to start the calculation at. * @param targetLevel The level to reach. * @return The amount of additional experience points required to go from the starting level to the target level. */ public static int getPointsForLevel(int startingLevel, int targetLevel) { if (targetLevel < startingLevel) { throw new IllegalArgumentException("Starting level must be lower than the target level!"); } else if (startingLevel < 0) { throw new IllegalArgumentException("Level bounds must be positive!"); } // If the levels are the same there is no point difference. else if (targetLevel == startingLevel) { return 0; } int requiredPoints = 0; for (int lvl = startingLevel + 1; lvl <= targetLevel; lvl++) { requiredPoints += getPointForLevel(lvl); } return requiredPoints; } /** * Calculates the total amount of experience points required to reach a given level when starting at level 0. * * @param level The target level to reach. * @return The amount of experience points required to reach the target level. */ public static int getTotalPointsForLevel(int level) { return getPointsForLevel(0, level); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/FunctionHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import com.mojang.datafixers.util.Either; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; public class FunctionHelper { /** * Tests an optional value. If the value is empty or the test fails it will return false. * * @param input The input value to test. * @param test The test to perform. * @param The type of value to test. * @return If the test was successful. */ public static boolean test(Optional input, Predicate test) { return input.isEmpty() || test.test(input.get()); } /** * Unpacks an Either into its value using the first possible match. * * @param either The Either to resolve. * @param The type of value held by the Either. * @return The first value that was unpacked. */ public static T unpack(Either either) { return either.map(Function.identity(), Function.identity()); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/IGameplayHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.RandomSource; import net.minecraft.world.Container; import net.minecraft.world.WorldlyContainerHolder; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.HopperBlockEntity; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; import java.util.function.BiFunction; public interface IGameplayHelper { RandomSource RNG = RandomSource.create(); /** * Gets the crafting remainder for a given item. This is required as some platforms have different logic for * determining the crafting remainder. * * @param input The input item. * @return The crafting remainder, or empty if none. */ default ItemStack getCraftingRemainder(ItemStack input) { if (input.getItem().hasCraftingRemainingItem()) { final Item remainder = input.getItem().getCraftingRemainingItem(); if (remainder != null) { return remainder.getDefaultInstance(); } } return ItemStack.EMPTY; } /** * If an inventory exists at the specified position, attempt to insert the item into all available slots until the * item has been fully inserted or no more slots are available. * * @param level The world instance. * @param pos The position of the block. * @param side The side you are accessing the inventory from. This is from the perspective of the inventory, not * your block. For example a hopper on top of a chest is inserting downwards but would use the upwards * face because that is the side of the chest being accessed. * @param stack The item to try inserting. * @return The remaining items that were not inserted. */ default ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) { if (stack.isEmpty()) { return stack; } final Container container = getContainer(level, pos); return container != null ? HopperBlockEntity.addItem(null, container, stack, side) : stack; } /** * Gets a vanilla container for a given position. This method supports block based containers like the composter, * and block entity based containers like a chest or barrel. * * @param level The world instance. * @param pos The position to check. * @return The container that was found, or null if no container exists. */ @Nullable default Container getContainer(ServerLevel level, BlockPos pos) { final BlockState state = level.getBlockState(pos); if (state.getBlock() instanceof WorldlyContainerHolder holder) { return holder.getContainer(state, level, pos); } final BlockEntity be = level.getBlockEntity(pos); if (be instanceof Container beContainer) { return beContainer; } return null; } /** * Attempts to add an item to a list based inventory. This code will try to insert into all available slots until * the item has been completely inserted or no items remain. * * @param stack The item to add into the inventory. * @param inventory The list of items to add to. * @param slots An array of valid slots to add to. * @return The remaining items that were not inserted. */ default ItemStack addItem(ItemStack stack, NonNullList inventory, int[] slots) { for (int slot : slots) { if (stack.isEmpty()) { return stack; } final ItemStack existing = inventory.get(slot); if (existing.isEmpty()) { inventory.set(slot, stack); return ItemStack.EMPTY; } else if (existing.getCount() < existing.getMaxStackSize() && ItemStack.isSameItemSameComponents(existing, stack)) { final int availableSpace = existing.getMaxStackSize() - existing.getCount(); final int movedAmount = Math.min(stack.getCount(), availableSpace); stack.shrink(movedAmount); existing.grow(movedAmount); } } return stack; } /** * Creates a new block entity builder using platform specific code. This is required because the underlying block * entity factory is not accessible. * * @param factory A factory that creates a new block entity instance. * @param validBlocks The array of valid blocks for the block entity. * @param The type of the block entity. * @return A new builder for your block entity type. */ BlockEntityType.Builder blockEntityBuilder(BiFunction factory, Block... validBlocks); /** * Drops the crafting remainder of an item into the world if the item has one. * * @param level The world to drop the item within. * @param pos The position to spawn the items at. * @param old The base item to spawn a remainder from. */ default void dropRemainders(Level level, BlockPos pos, ItemStack old) { if (!level.isClientSide && !old.isEmpty()) { final ItemStack remainder = this.getCraftingRemainder(old); if (!remainder.isEmpty()) { Block.popResource(level, pos, remainder.copy()); } } } CreativeModeTab.Builder tabBuilder(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/IPlatformHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.darkhax.bookshelf.common.api.ModEntry; import net.darkhax.bookshelf.common.api.PhysicalSide; import java.io.File; import java.nio.file.Path; import java.util.Set; /** * The PlatformHelper provides useful context and information about the platform the game is running on. */ public interface IPlatformHelper { /** * Gets the working directory path of the game directory. * * @return The working directory path of the game directory. */ Path getGamePath(); /** * Gets the working directory of the game as a File. * * @return The working directory of the game. */ default File getGameDirectory() { return this.getGamePath().toFile(); } /** * Gets the specified configuration path for the game. * * @return The specified configuration path for the game. */ Path getConfigPath(); /** * Gets the specified configuration directory as a file reference. * * @return The specified configuration path for the game. */ default File getConfigDirectory() { return this.getConfigPath().toFile(); } /** * Gets the primary path that the current loader will load mods from. * * @return The currently specified mods path. */ Path getModsPath(); /** * Gets the primary directory that the current loader will load mods from. * * @return The currently specified mods directory. */ default File getModsDirectory() { return this.getModsPath().toFile(); } /** * Checks if a given mod is loaded. * * @param modId The mod id to search for. * @return True when the specified mod id has been loaded. */ boolean isModLoaded(String modId); /** * Checks if the mod is running in a development environment. * * @return True when the mod is running in a developer environment. */ boolean isDevelopmentEnvironment(); /** * Gets the physical environment that the code is running on. * * @return The physical environment that the code is running on. */ PhysicalSide getPhysicalSide(); /** * Checks if the code is running on a physical client. * * @return Returns true when the code is running on a physical client. */ default boolean isPhysicalClient() { return this.getPhysicalSide().isClient(); } /** * Gets a set of every loaded modId. * * @return A set of all loaded mods. */ Set getLoadedMods(); /** * Checks if the mod is currently running in an environment with game tests enabled. * * @return Are game tests currently enabled? */ boolean isTestingEnvironment(); /** * Gets the name of the platform. * * @return The name of the platform. */ String getName(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/IRenderHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import net.darkhax.bookshelf.common.api.service.Services; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.inventory.InventoryMenu; import net.minecraft.world.level.Level; import net.minecraft.world.level.material.FluidState; import org.joml.Matrix4f; public interface IRenderHelper { IRenderHelper GET = Services.load(IRenderHelper.class); default TextureAtlasSprite blockSprite(ResourceLocation texturePath) { return Minecraft.getInstance().getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(texturePath); } void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay); default int[] unpackARGB(int color) { return new int[]{color >> 24 & 0xff, color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff}; } default void renderBox(VertexConsumer builder, PoseStack stack, TextureAtlasSprite sprite, int light, int overlay, int[] color) { renderBox(builder, stack.last().pose(), sprite, light, overlay, 0f, 1f, 0f, 1f, 0f, 1f, color); } default void renderBox(VertexConsumer builder, PoseStack stack, TextureAtlasSprite sprite, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) { renderBox(builder, stack.last().pose(), sprite, light, overlay, x1, x2, y1, y2, z1, z2, color); } default void renderBox(VertexConsumer builder, Matrix4f pos, TextureAtlasSprite sprite, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) { renderFace(builder, pos, sprite, Direction.DOWN, light, overlay, x1, x2, y1, y2, z1, z2, color); renderFace(builder, pos, sprite, Direction.UP, light, overlay, x1, x2, y1, y2, z1, z2, color); renderFace(builder, pos, sprite, Direction.NORTH, light, overlay, x1, x2, y1, y2, z1, z2, color); renderFace(builder, pos, sprite, Direction.SOUTH, light, overlay, x1, x2, y1, y2, z1, z2, color); renderFace(builder, pos, sprite, Direction.WEST, light, overlay, x1, x2, y1, y2, z1, z2, color); renderFace(builder, pos, sprite, Direction.EAST, light, overlay, x1, x2, y1, y2, z1, z2, color); } default void renderFace(VertexConsumer builder, Matrix4f pos, TextureAtlasSprite sprite, Direction side, int light, int overlay, float x1, float x2, float y1, float y2, float z1, float z2, int[] color) { switch (side) { case DOWN -> { final float u1 = sprite.getU(x1); final float u2 = sprite.getU(x2); final float v1 = sprite.getV(z1); final float v2 = sprite.getV(z2); builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f); builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f); builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f); builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, -1f, 0f); } case UP -> { final float u1 = sprite.getU(x1); final float u2 = sprite.getU(x2); final float v1 = sprite.getV(z1); final float v2 = sprite.getV(z2); builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f); builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f); builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f); builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 1f, 0f); } case NORTH -> { final float u1 = sprite.getU(x1); final float u2 = sprite.getU(x2); final float v1 = sprite.getV(y1); final float v2 = sprite.getV(y2); builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f); builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f); builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f); builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, -1f); } case SOUTH -> { final float u1 = sprite.getU(x1); final float u2 = sprite.getU(x2); final float v1 = sprite.getV(y1); final float v2 = sprite.getV(y2); builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f); builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f); builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f); builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(0f, 0f, 1f); } case WEST -> { final float u1 = sprite.getU(y1); final float u2 = sprite.getU(y2); final float v1 = sprite.getV(z1); final float v2 = sprite.getV(z2); builder.addVertex(pos, x1, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f); builder.addVertex(pos, x1, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f); builder.addVertex(pos, x1, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f); builder.addVertex(pos, x1, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(-1f, 0f, 0f); } case EAST -> { final float u1 = sprite.getU(y1); final float u2 = sprite.getU(y2); final float v1 = sprite.getV(z1); final float v2 = sprite.getV(z2); builder.addVertex(pos, x2, y1, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v1).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f); builder.addVertex(pos, x2, y2, z1).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v1).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f); builder.addVertex(pos, x2, y2, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u2, v2).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f); builder.addVertex(pos, x2, y1, z2).setColor(color[1], color[2], color[3], color[0]).setUv(u1, v2).setOverlay(overlay).setLight(light).setNormal(1f, 0f, 0f); } } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/MathsHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.util.RandomSource; import net.minecraft.world.level.block.Block; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.VoxelShape; import java.math.BigDecimal; import java.math.RoundingMode; import java.security.SecureRandom; import java.text.DecimalFormat; import java.util.Arrays; import java.util.EnumMap; import java.util.Map; import java.util.Random; public class MathsHelper { /** * An RNG source that can be used in contexts where a more suitable RNG source is not available. */ public static final Random RANDOM = new SecureRandom(); /** * A RandomSource that can be used in contexts where a more suitable RNG source is not available. */ public static final RandomSource RANDOM_SOURCE = RandomSource.create(); /** * A decimal format that will only preserve two decimal places. */ public static final DecimalFormat DECIMAL_2 = new DecimalFormat("##.##"); /** * Checks if a double is within the given range. * * @param min The smallest value that is valid. * @param max The largest value that is valid. * @param value The value to check. * @return If the value is within the defined range. */ public static boolean inRange(double min, double max, double value) { return value <= max && value >= min; } /** * Calculates the distance between two points. * * @param first The first position. * @param second The second position. * @return The distance between the first and second position. */ public static double distance(Vec3 first, Vec3 second) { final double distanceX = first.x - second.x; final double distanceY = first.y - second.y; final double distanceZ = first.z - second.z; return Math.sqrt(distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ); } /** * Rounds a double with a certain amount of precision. * * @param value The value to round. * @param places The amount of decimal places to preserve. * @return The rounded value. */ public static double round(double value, int places) { return value >= 0 && places > 0 ? BigDecimal.valueOf(value).setScale(places, RoundingMode.HALF_UP).doubleValue() : value; } /** * Generates a pseudorandom number within a given range of values. The range of values is inclusive of the minimum * and maximum value. * * @param rng The RNG source to generate the number. * @param min The minimum value to generate. * @param max The maximum value to generate. * @return A pseudorandom number within the provided range. */ public static int nextInt(Random rng, int min, int max) { return rng.nextInt(max - min + 1) + min; } /** * Generates a pseudorandom number within a given range of values. The range of values is inclusive of the minimum * and maximum value. * * @param rng The RNG source to generate the number. * @param min The minimum value to generate. * @param max The maximum value to generate. * @return A pseudorandom number within the provided range. */ public static int nextInt(RandomSource rng, int min, int max) { return rng.nextIntBetweenInclusive(min, max); } /** * Performs an RNG check that has a percent chance to succeed. * * @param chance The chance that the check will succeed. * @return Returns true when the RNG check is successful. */ public static boolean percentChance(double chance) { return Math.random() < chance; } /** * Calculates the average of many integers. * * @param values The values to average. * @return The average of the input values. */ public static float average(int... values) { return Arrays.stream(values).sum() / (float) values.length; } /** * Calculates the percentage out of a total. * * @param value The value that is available. * @param total The largest possible value. * @return The calculated percentage. */ public static float percentage(int value, int total) { return (float) value / (float) total; } /** * Converts a standard pixel measurement to a world-space measurement. This assumes one block in the world * represents 16 pixels. * * @param pixels The amount of pixels. * @return The size of the pixels in the world-space. */ public static double pixelSize(int pixels) { return pixels / 16d; } /** * Creates an Axis Aligned Bounding Box from a series of pixel measurements. * * @param minX The start on the X axis. * @param minY The start on the Y axis. * @param minZ The start on the Z axis. * @param maxX The end on the X axis. * @param maxY The end on the Y axis. * @param maxZ The end on the Z axis. * @return An AABB that represents a series of block pixel measurements. */ public static AABB boundsForPixels(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { return new AABB(pixelSize(minX), pixelSize(minY), pixelSize(minZ), pixelSize(maxX), pixelSize(maxY), pixelSize(maxZ)); } /** * Creates horizontally rotated variants of a VoxelShape. The input values are considered to be rotated north. * * @param minX The min X of the shape. * @param minY The min Y of the shape. * @param minZ The min Z of the shape. * @param maxX The max X of the shape. * @param maxY The max Y of the shape. * @param maxZ The max Z of the shape. * @return A map of rotated VoxelShape. */ public static Map createHorizontalShapes(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { final Map shapes = new EnumMap<>(Direction.class); Direction.Plane.HORIZONTAL.forEach(dir -> shapes.put(dir, rotateShape(dir, minX, minY, minZ, maxX, maxY, maxZ))); return shapes; } /** * Creates a VoxelShape that has been rotated to face a given direction. The input sizes are considered to be * rotated north already. The up/down rotations are not supported yet. * * @param facing The direction to rotate the shape. * @param x1 The min x coordinate. * @param y1 The min y coordinate. * @param z1 The min z coordinate. * @param x2 The max x coordinate. * @param y2 The max y coordinate. * @param z2 The max z coordinate. * @return The rotated VoxelShape. */ public static VoxelShape rotateShape(Direction facing, double x1, double y1, double z1, double x2, double y2, double z2) { return switch (facing) { case NORTH -> Block.box(x1, y1, z1, x2, y2, z2); case EAST -> Block.box(16 - z2, y1, x1, 16 - z1, y2, x2); case SOUTH -> Block.box(16 - x2, y1, 16 - z2, 16 - x1, y2, 16 - z1); case WEST -> Block.box(z1, y1, 16 - x2, z2, y2, 16 - x1); default -> throw new IllegalArgumentException("Can not rotate face in direction " + facing.name()); }; } /** * Offsets a position horizontally by a random amount. * * @param startPos The starting position to offset from. * @param rng The RNG source. * @param range The maximum amount of blocks to offset the position by. This range applies to both the positive * and negative directions. * @return The randomly offset position. */ public static BlockPos randomOffsetHorizontal(BlockPos startPos, RandomSource rng, int range) { return randomOffset(startPos, rng, range, 0, range); } /** * Offsets a position by a random amount within a limited range. * * @param startPos The starting position to offset from. * @param rng The RNG source. * @param rangeX The maximum amount of blocks to offset on the X axis. * @param rangeY The maximum amount of blocks to offset on the Y axis. * @param rangeZ The maximum amount of blocks to offset on the Z axis. * @return The randomly offset position. */ public static BlockPos randomOffset(BlockPos startPos, RandomSource rng, int rangeX, int rangeY, int rangeZ) { if (rangeX < 0 || rangeY < 0 || rangeZ < 0) { throw new IllegalArgumentException("Cannot offset position by '" + rangeX + ", " + rangeY + ", " + rangeZ + "'. Range must be positive!"); } final int offsetX = rangeX != 0 ? rng.nextIntBetweenInclusive(-rangeX, rangeX) : 0; final int offsetY = rangeY != 0 ? rng.nextIntBetweenInclusive(-rangeY, rangeY) : 0; final int offsetZ = rangeZ != 0 ? rng.nextIntBetweenInclusive(-rangeZ, rangeZ) : 0; return startPos.offset(offsetX, offsetY, offsetZ); } /** * Encodes an array of bytes into an array of integers. * * @param bytes The bytes to encode. * @return An array of integers that contain the byte data. */ public static int[] encodeBytesToInt(byte[] bytes) { final int byteCount = bytes.length; final int msgSize = (byteCount + 3) / 4; final int encodedLength = 1 + msgSize; final int[] result = new int[encodedLength]; result[0] = byteCount; // Store the length in the first int for (int i = 0; i < msgSize; i++) { int value = 0; for (int j = 0; j < 4; j++) { final int byteIndex = i * 4 + j; if (byteIndex < byteCount) { value |= (bytes[byteIndex] & 0xFF) << (24 - j * 8); } } result[i + 1] = value; // Offset by 1 due to length header } return result; } /** * Decodes an array of bytes from an array of integers. * * @param data The data to decode. * @return The decoded bytes. */ public static byte[] decodeBytesFromInt(int[] data) { if (data.length == 0) { return new byte[0]; } final int byteCount = data[0]; final byte[] result = new byte[byteCount]; for (int i = 0; i < (data.length - 1); i++) { final int value = data[i + 1]; for (int j = 0; j < 4; j++) { final int byteIndex = i * 4 + j; if (byteIndex < byteCount) { result[byteIndex] = (byte) ((value >> (24 - j * 8)) & 0xFF); } } } return result; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/TextHelper.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.darkhax.bookshelf.common.api.PhysicalSide; import net.darkhax.bookshelf.common.api.annotation.OnlyFor; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.api.text.unit.Units; import net.darkhax.bookshelf.common.mixin.access.client.AccessorFontManager; import net.darkhax.bookshelf.common.mixin.access.client.AccessorMinecraft; import net.darkhax.bookshelf.common.mixin.access.entity.AccessorEntity; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.language.I18n; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import net.minecraft.util.StringUtil; import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; public class TextHelper { /** * Creates translated text from a resource location. * * @param prefix The prefix to add at the start of the key. * @param suffix The suffix to add at the end of the key. * @param location The resource location to use in the key. * @param args An optional array of arguments to format into the translated text. * @return A translated component based on the resource location. */ public static MutableComponent fromResourceLocation(@Nullable String prefix, @Nullable String suffix, ResourceLocation location, Object... args) { final StringBuilder builder = new StringBuilder(); if (prefix != null) { builder.append(prefix).append("."); } builder.append(location.getNamespace()).append(".").append(location.getPath()); if (suffix != null) { builder.append(".").append(suffix); } return Component.translatable(builder.toString(), args); } /** * Formats a duration of time in ticks into its real world time counterpart. This method should ONLY be used if a * world context is not available or if the duration of time is not affected by custom world tick rates. * * @param ticks The duration of ticks. * @return The formatted time duration. */ public static MutableComponent formatDuration(int ticks) { return formatDuration(ticks, true, 1f); } /** * Formats a duration of time in ticks into its real world time counterpart. * * @param ticks The duration of ticks. * @param level The world level that the duration is taking place. This is used to account for custom tick rates set * using the game rule. * @return The formatted time duration. */ public static MutableComponent formatDuration(int ticks, Level level) { return formatDuration(ticks, true, level); } /** * Formats a duration of time in ticks into its real world time counterpart. * * @param ticks The duration of ticks. * @param includeHover Should the raw tick amount be shown when hovering the text? * @param level The world level that the duration is taking place. This is used to account for custom tick * rates set using the game rule. * @return The formatted time duration. */ public static MutableComponent formatDuration(int ticks, boolean includeHover, Level level) { return formatDuration(ticks, includeHover, level.tickRateManager().tickrate()); } /** * Formats a duration of time in ticks into its real world time counterpart. * * @param ticks The duration of ticks. * @param showTicksOnHover Should the raw tick amount be shown when hovering the text? * @param tickRate The tick rate of the current world. * @return The formatted time duration. */ public static MutableComponent formatDuration(int ticks, boolean showTicksOnHover, float tickRate) { MutableComponent timeText = Component.literal(StringUtil.formatTickDuration(ticks, tickRate)); if (showTicksOnHover) { timeText = Units.TICK.format(ticks); } return timeText; } /** * Applies hover text to a text component. * * @param base The base text component to append. * @param hover The text to display while hovering. * @return A component instance with the hover event applied. */ public static MutableComponent withHover(Component base, Component hover) { return withHover(base, new HoverEvent(HoverEvent.Action.SHOW_TEXT, hover)); } /** * Applies hover text based on an entity to a text component. * * @param base The base text component to append. * @param hover The Entity to display in the hover text. * @return A component instance with the hover event applied. */ public static MutableComponent withHover(Component base, Entity hover) { return withHover(base, hoverEvent(hover)); } /** * Applies hover text based on an item to a text component. * * @param base The base text component to append. * @param hover The ItemStack to display in the hover text. * @return A component instance with the hover event applied. */ public static MutableComponent withHover(Component base, ItemStack hover) { return withHover(base, new HoverEvent(HoverEvent.Action.SHOW_ITEM, new HoverEvent.ItemStackInfo(hover))); } /** * Applies a hover event to a text component. * * @param base The base text component to append. * @param hover The hover event to apply. * @return A component instance with the hover event applied. */ public static MutableComponent withHover(Component base, HoverEvent hover) { return mutable(base).withStyle(style -> style.withHoverEvent(hover)); } /** * Creates a new hover event for an entity. * * @param entity The entity to create a HoverEvent for. * @return When Mixins are available the entity will create its own HoverEvent, otherwise a fallback based on the * default implementation will be used. */ public static HoverEvent hoverEvent(Entity entity) { if (entity instanceof AccessorEntity access) { return access.bookshelf$createHoverEvent(); } return new HoverEvent(HoverEvent.Action.SHOW_ENTITY, new HoverEvent.EntityTooltipInfo(entity.getType(), entity.getUUID(), entity.getName())); } /** * Provides mutable access to a component. * * @param component The component to access. * @return If the component is already mutable the same component instance will be returned. Otherwise, a mutable * copy of the component will be created. */ public static MutableComponent mutable(Component component) { return component instanceof MutableComponent mutable ? mutable : component.copy(); } /** * Recursively applies a font to text and all of its subcomponents. * * @param text The text to apply the font to. * @param font The ID of the font to apply. * @return The input text with the font applied to its style and the style of its subcomponents. */ public static Component applyFont(Component text, ResourceLocation font) { if (text == CommonComponents.EMPTY) { return text; } final MutableComponent modified = mutable(text); modified.withStyle(style -> style.withFont(font)); modified.getSiblings().forEach(sib -> applyFont(sib, font)); return modified; } /** * Attempts to localize several different translation keys and will return the first one that is available on the * client. If no keys are mapped the result will be null. * * @param id An ID the format within each key using basic string formatting. The first parameter is the namespace * and the second is the path. For example if a key was "tooltip.{0}.{1}.info" the ID "minecraft:stick" * will produce a final key of "tooltip.minecraft.stick.info". * @param keys An array of translation keys to attempt localizing. * @return A component for the first translation key that is mapped, or null if none of the keys are mapped. */ @Nullable @OnlyFor(PhysicalSide.CLIENT) public static MutableComponent lookupTranslationWithAlias(ResourceLocation id, String... keys) { for (String key : keys) { final MutableComponent lookupResult = lookupTranslation(key.formatted(id.getNamespace(), id.getPath())); if (lookupResult != null) { return lookupResult; } } return null; } /** * Attempts to localize several different translation keys and will return the first one that is available on the * client. If no keys are mapped the result will be null. * * @param keys An array of translation keys to attempt localizing. * @param params Arguments that are passed into the translated text. * @return A component for the first translation key that is mapped, or null if none of the keys are mapped. */ @Nullable @OnlyFor(PhysicalSide.CLIENT) public static MutableComponent lookupTranslationWithAlias(String[] keys, Object... params) { for (String key : keys) { final MutableComponent lookupResult = lookupTranslation(key, params); if (lookupResult != null) { return lookupResult; } } return null; } /** * Attempts to localize text. If the translation key is not mapped on the client the component will be null. * * @param key The translation key to localize. * @param args Arguments that are passed into the translated text. * @return If the key can be translated a component will be returned, otherwise null. */ @Nullable @OnlyFor(PhysicalSide.CLIENT) public static MutableComponent lookupTranslation(String key, Object... args) { return lookupTranslation(key, (s, o) -> null, args); } /** * Attempts to localize text. If the translation key is not mapped on the client the fallback will be used. * * @param key The translation key to localize. * @param fallback The fallback text to use when the key is unavailable. * @param args Arguments that are passed into the translated text. * @return If the key can be translated a component will be returned, otherwise the fallback will be used. */ @Nullable @OnlyFor(PhysicalSide.CLIENT) public static MutableComponent lookupTranslation(String key, MutableComponent fallback, Object... args) { return lookupTranslation(key, (s, o) -> fallback, args); } /** * Attempts to localize text. If the translation key is not mapped on the client it will try to use the fallback. * * @param key The translation key to localize. * @param fallback A function that provides fallback text based on the original translation key and arguments. Both * the function and the result of this function may be null. * @param args Arguments that are passed into the translated text. * @return If the key can be translated a component will be returned, otherwise the fallback will be used. */ @Nullable @OnlyFor(PhysicalSide.CLIENT) public static MutableComponent lookupTranslation(String key, @Nullable BiFunction fallback, Object... args) { if (!Services.PLATFORM.isPhysicalClient()) { throw new IllegalStateException("Text can not be translated on the server."); } return I18n.exists(key) ? Component.translatable(key, args) : fallback != null ? fallback.apply(key, args) : null; } /** * Creates a text component that will copy the value to the players clipboard when they click it. * * @param text The text to display and copy to the clipboard. * @return A component that displays text and copies that text to the clipboard when the player clicks on it. */ public static MutableComponent copyText(String text) { return setCopyText(Component.literal(text), text); } /** * Adds a click event to a text component that will copy text to the players clipboard when they click on it. * * @param component The component to attack the click event to. * @param copy The text to be copied to the clipboard. * @return A text component that will copy the text when the player clicks on it. */ public static MutableComponent setCopyText(MutableComponent component, String copy) { return component.withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, copy))); } /** * Joins several components together using a separator. * * @param separator The separator to insert between other components. * @param toJoin The components to join together. * @return A component containing the joint components. */ public static MutableComponent join(Component separator, Component... toJoin) { return join(separator, Arrays.stream(toJoin).iterator()); } /** * Joins several components together using a separator. * * @param separator The separator to insert between other components. * @param toJoin The components to join together. * @return A component containing the joint components. */ public static MutableComponent join(Component separator, Collection toJoin) { return join(separator, toJoin.iterator()); } /** * Joins several components together using a separator. Duplicate entries will be ignored and only the first * occurrence will be joint. * * @param separator The separator to insert between other components. * @param toJoin The components to join together. * @return A component containing the joint components. */ public static MutableComponent joinUnique(Component separator, Collection toJoin) { final Set entries = new HashSet<>(); for (Component toAdd : toJoin) { addUnique(entries, toAdd); } return join(separator, entries); } /** * Adds a component to a list, only if the list does not already contain that component. * * @param components The list to add to. * @param toAdd The component to add. * @return If the component was added or not. */ public static boolean addUnique(Collection components, Component toAdd) { for (Component existing : components) { if (Objects.equals(existing, toAdd) || existing.getContents().equals(toAdd.getContents())) { return false; } } components.add(toAdd); return true; } /** * Joins several components together using a separator. * * @param separator The separator to insert between other components. * @param toJoin The components to join together. * @return A component containing the joint components. */ public static MutableComponent join(Component separator, Iterator toJoin) { final MutableComponent joined = Component.literal(""); while (toJoin.hasNext()) { joined.append(toJoin.next()); if (toJoin.hasNext()) { joined.append(separator); } } return joined; } /** * Finds a set of possible matches within an iterable group of strings. This can be used to take invalid user input * and attempt to find a plausible match using known good values. *

* Possible matches are determined using the Levenshtein distance between the input value and the potential * candidates. The Levenshtein distance represents the number of characters that need to be changed in order for the * strings to match. For example "abc" to "def" has a difference of three, while "123" to "1234" has a distance of * 1. * * @param input The input string. * @param candidates An iterable group of possible candidates. * @return A set of possible matches for the input. This set will include all candidates that have the lowest * possible distance. For example if there were 100 candidates and five had a distance of one all five of the lowest * distance values will be returned. */ public static Set getPossibleMatches(String input, Iterable candidates) { return getPossibleMatches(input, candidates, Integer.MAX_VALUE); } /** * Finds a set of possible matches within an iterable group of strings. This can be used to take invalid user input * and attempt to find a plausible match using known good values. *

* Possible matches are determined using the Levenshtein distance between the input value and the potential * candidates. The Levenshtein distance represents the number of characters that need to be changed in order for the * strings to match. For example "abc" to "def" has a difference of three, while "123" to "1234" has a distance of * 1. * * @param input The input string. * @param candidates An iterable group of possible candidates. * @param threshold The maximum distance allowed for a value to be considered. For example if the threshold is two, * only entries with a distance of two or less will be considered. * @return A set of possible matches for the input. This set will include all candidates that have the lowest * possible distance. For example if there were 100 candidates and five had a distance of one all five of the lowest * distance values will be returned. */ public static Set getPossibleMatches(String input, Iterable candidates, int threshold) { final HashSet bestMatches = new HashSet(); int distance = threshold; for (String candidate : candidates) { final int currentDistance = StringUtils.getLevenshteinDistance(input, candidate); if (currentDistance < distance) { distance = currentDistance; bestMatches.clear(); bestMatches.add(candidate); } else if (currentDistance == distance) { bestMatches.add(candidate); } } return bestMatches; } /** * Formats a collection of values to a string using {@link Object#toString()}. If the collection has more than one * value each entry will be separated by commas. Each value will also be quoted. * * @param collection The collection of values to format. * @param The type of value being formatted. * @return The formatted string. */ public static String formatCollection(Collection collection) { return formatCollection(collection, entry -> "\"" + entry.toString() + "\"", ", "); } /** * Formats a collection of values to a string. If the collection has more than one value each entry will be * separated using the delimiter. * * @param collection The collection of values to format. * @param formatter A function used to format the value to a string. * @param delimiter A delimiter used to separate values in a list. * @param The type of value being formatted. * @return The formatted string. */ public static String formatCollection(Collection collection, Function formatter, String delimiter) { return collection.size() == 1 ? formatter.apply(collection.stream().findFirst().get()) : collection.stream().map(formatter).collect(Collectors.joining(delimiter)); } @OnlyFor(PhysicalSide.CLIENT) public static Set getRegisteredFonts() { if (!Services.PLATFORM.isPhysicalClient()) { return Collections.emptySet(); } return ((AccessorFontManager) (((AccessorMinecraft) Minecraft.getInstance()).bookshelf$getFontManager())).bookshelf$getFonts().keySet(); } /** * Creates a translation key that should map to a display name for the tag. *

* Tags for vanilla registries use the format tag.reg_path.namespace.path and tags for modded registries use the * format tag.reg_namespace.reg_path.namespace.path. *

* This is a new standard being pushed by the Fabric API and recipe viewers. While it has not been universally * adopted yet, it should be considered best practice to do so moving forward. * * @param tag The tag to provide a name key for. * @return A translation key that should map to a display name. */ public static String getTagName(TagKey tag) { final StringBuilder builder = new StringBuilder(); builder.append("tag."); final ResourceLocation regId = tag.registry().location(); final ResourceLocation tagId = tag.location(); if (!regId.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { builder.append(regId.getNamespace()).append("."); } builder.append(regId.getPath().replace("/", ".")).append(".").append(tagId.getNamespace()).append(".").append(tagId.getPath().replace("/", ".").replace(":", ".")); return builder.toString(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/api/util/TickAccumulator.java ================================================ package net.darkhax.bookshelf.common.api.util; import net.minecraft.world.level.Level; /** * While the current tick rate is synced between the client and server, some things like tile entities continue to tick * at the base 20tps on the client. This tick accumulator allows ticks to be accumulates as normal the server, but will * scale client ticks to roughly the correct amount. */ public class TickAccumulator { private final float defaultValue; private float ticks; /** * Creates a new tick accumulator. * * @param defaultValue The amount to reset to when {@link TickAccumulator#reset()} is used. */ public TickAccumulator(float defaultValue) { this.ticks = defaultValue; this.defaultValue = defaultValue; } /** * Ticks the accumulator up by one tick. * * @param level The current game level. */ public void tickUp(Level level) { this.tick(level.isClientSide ? level.tickRateManager().tickrate() / 20f : 1f); } /** * Ticks the accumulator down by one tick. * * @param level The current game level. */ public void tickDown(Level level) { this.tick(-(level.isClientSide ? level.tickRateManager().tickrate() / 20f : 1f)); } /** * Adds an amount of ticks to the accumulator. * * @param amount The amount of ticks to add. */ public void tick(float amount) { this.ticks += amount; } /** * Get the current amount of ticks. * * @return The current amount of ticks. */ public float getTicks() { return this.ticks; } /** * Sets the current amount of ticks. * * @param ticks The new tick count. */ public void setTicks(float ticks) { this.ticks = ticks; } /** * Resets the tick accumulator. */ public void reset() { this.ticks = this.defaultValue; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/BookshelfContent.java ================================================ package net.darkhax.bookshelf.common.impl; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.darkhax.bookshelf.common.api.commands.args.FontArgument; import net.darkhax.bookshelf.common.api.commands.args.TagArgument; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriptions; import net.darkhax.bookshelf.common.api.registry.ContentProvider; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.command.BlockTagToItemTagCommand; import net.darkhax.bookshelf.common.impl.command.DebugCommands; import net.darkhax.bookshelf.common.impl.command.EnchantCommand; import net.darkhax.bookshelf.common.impl.command.FontCommand; import net.darkhax.bookshelf.common.impl.command.HandCommand; import net.darkhax.bookshelf.common.impl.command.RenameCommand; import net.darkhax.bookshelf.common.impl.command.StructureCommand; import net.darkhax.bookshelf.common.impl.command.TranslateCommand; import net.darkhax.bookshelf.common.impl.data.conditions.And; import net.darkhax.bookshelf.common.impl.data.conditions.ModLoaded; import net.darkhax.bookshelf.common.impl.data.conditions.Not; import net.darkhax.bookshelf.common.impl.data.conditions.OnPlatform; import net.darkhax.bookshelf.common.impl.data.conditions.Or; import net.darkhax.bookshelf.common.impl.data.conditions.RegistryContains; import net.darkhax.bookshelf.common.impl.data.criterion.item.NamespaceItemPredicate; import net.darkhax.bookshelf.common.impl.data.criterion.trigger.AdvancementTrigger; import net.darkhax.bookshelf.common.impl.data.ingredient.AllOfIngredient; import net.darkhax.bookshelf.common.impl.data.ingredient.BlockTagIngredient; import net.darkhax.bookshelf.common.impl.data.ingredient.EitherIngredient; import net.darkhax.bookshelf.common.impl.data.ingredient.FalseIngredient; import net.darkhax.bookshelf.common.impl.data.ingredient.ModIdIngredient; import net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack; import net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootDescriptionAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter; import net.minecraft.advancements.CriterionTrigger; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.level.storage.loot.entries.LootPoolEntries; public class BookshelfContent implements ContentProvider { @Override public void defineIngredientTypes(IngredientTypeAdapter registry) { registry.add("false", FalseIngredient.CODEC, FalseIngredient.STREAM); registry.add("all", AllOfIngredient.CODEC, AllOfIngredient.STREAM); registry.add("either", EitherIngredient.CODEC, EitherIngredient.STREAM); registry.add("mod_id", ModIdIngredient.CODEC, ModIdIngredient.STREAM); registry.add("block_tag", BlockTagIngredient.CODEC, BlockTagIngredient.STREAM); } @Override public void defineCommands(CommandDispatcher dispatcher, CommandBuildContext context, Commands.CommandSelection selection) { final LiteralArgumentBuilder root = Commands.literal(Constants.MOD_ID).requires(PermissionLevel.MODERATOR); root.then(HandCommand.build(context)); root.then(FontCommand.build()); root.then(RenameCommand.build(context)); root.then(EnchantCommand.build(context)); root.then(TranslateCommand.build(context)); root.then(BlockTagToItemTagCommand.build(context)); root.then(StructureCommand.build()); if (Services.PLATFORM.isDevelopmentEnvironment() && Services.PLATFORM.isPhysicalClient() && selection == Commands.CommandSelection.INTEGRATED) { root.then(DebugCommands.build(context)); } dispatcher.register(root); } @Override public void defineCommandArguments(CommandArgumentAdapter registry) { registry.add("font", FontArgument.class, FontArgument.SERIALIZER); registry.add("tag", TagArgument.class, TagArgument.SERIALIZER); } @Override public void defineLoadConditions(GenericRegistryAdapter> registry) { registry.add(And.TYPE_ID, And.CODEC); registry.add(Not.TYPE_ID, Not.CODEC); registry.add(Or.TYPE_ID, Or.CODEC); registry.add(OnPlatform.TYPE_ID, OnPlatform.CODEC); registry.add(ModLoaded.TYPE_ID, ModLoaded.CODEC); registry.add(RegistryContains.BLOCK, RegistryContains.of(RegistryContains.BLOCK, BuiltInRegistries.BLOCK)); registry.add(RegistryContains.ITEM, RegistryContains.of(RegistryContains.ITEM, BuiltInRegistries.ITEM)); registry.add(RegistryContains.ENTITY, RegistryContains.of(RegistryContains.ENTITY, BuiltInRegistries.ENTITY_TYPE)); registry.add(RegistryContains.BLOCK_ENTITY, RegistryContains.of(RegistryContains.BLOCK_ENTITY, BuiltInRegistries.BLOCK_ENTITY_TYPE)); } @Override public void defineItemSubPredicates(GameRegistryAdapter> registry) { registry.add("namespace", new ItemSubPredicate.Type<>(NamespaceItemPredicate.CODEC)); } @Override public void defineCriteriaTriggers(GameRegistryAdapter> registry) { registry.add("earn_advancement", AdvancementTrigger.TRIGGER); } @Override public void defineLootEntryTypes(LootEntryTypeAdapter registry) { registry.add("item_stack", LootItemStack.CODEC); } @Override public void defineLootDescriptions(LootDescriptionAdapter registry) { registry.registryFunc().accept(LootPoolEntries.EMPTY, LootPoolEntryDescriptions.EMPTY); registry.registryFunc().accept(LootPoolEntries.ITEM, LootPoolEntryDescriptions.ITEM); registry.registryFunc().accept(LootPoolEntries.LOOT_TABLE, LootPoolEntryDescriptions.LOOT_TABLE); registry.registryFunc().accept(LootPoolEntries.DYNAMIC, LootPoolEntryDescriptions.DYNAMIC); registry.registryFunc().accept(LootPoolEntries.TAG, LootPoolEntryDescriptions.TAG); registry.registryFunc().accept(LootPoolEntries.ALTERNATIVES, LootPoolEntryDescriptions.COMPOSITE); registry.registryFunc().accept(LootPoolEntries.SEQUENCE, LootPoolEntryDescriptions.COMPOSITE); registry.registryFunc().accept(LootPoolEntries.GROUP, LootPoolEntryDescriptions.COMPOSITE); registry.registryFunc().accept(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE.get(Constants.id("item_stack")), LootPoolEntryDescriptions.ITEM_STACK); } @Override public String namespace() { return Constants.MOD_ID; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/BookshelfMod.java ================================================ package net.darkhax.bookshelf.common.impl; import net.darkhax.bookshelf.common.api.service.Services; import java.io.IOException; import java.util.List; public class BookshelfMod { private static BookshelfMod instance; private boolean hasInitialized = false; public void init() { if (hasInitialized) { throw new IllegalStateException("The " + Constants.MOD_NAME + " has already been initialized."); } this.runStartupChecks(); hasInitialized = true; } private void runStartupChecks() { if (Services.PLATFORM == null) { throw new IllegalStateException("Bookshelf services are not available."); } this.detectInvalidContentProviders(); } @Deprecated private void detectInvalidContentProviders() { try { final List oldProviders = Services.findServices("net.darkhax.bookshelf.common.api.registry.IContentProvider"); if (!oldProviders.isEmpty()) { final String errorMsg = "An outdated implementation of IContentProvider has been found. The game is being stopped for your protection. Please check if an update is available! More information at https://gist.github.com/Darkhax/63356eed0a27848efe8574ce4c677bae"; Constants.LOG.error(errorMsg); for (String provider : oldProviders) { Constants.LOG.error("- {}", provider); } throw new IllegalStateException(errorMsg + " " + String.join(", ", oldProviders)); } } catch (IOException e) { Constants.LOG.error("Failed to read services.", e); } } /** * Gets the Bookshelf mod instance. If an instance does not already exist one will be created. * * @return The Bookshelf mod instance. */ public static BookshelfMod getInstance() { if (instance == null) { instance = new BookshelfMod(); } return instance; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/Constants.java ================================================ package net.darkhax.bookshelf.common.impl; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.crafting.RecipeManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.ref.WeakReference; public class Constants { public static final String MOD_ID = "bookshelf"; public static final String MOD_NAME = "Bookshelf"; public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME); public static final Gson GSON_PRETTY = new GsonBuilder().setPrettyPrinting().create(); public static ResourceLocation id(String path) { return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); } public static WeakReference SERVER_RECIPE_MANAGER; public static int SERVER_REVISION = 0; public static WeakReference CLIENT_RECIPE_MANAGER; public static int CLIENT_REVISION = 0; } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/DebugContentProvider.java ================================================ package net.darkhax.bookshelf.common.impl; import net.darkhax.bookshelf.common.api.entity.villager.MerchantTier; import net.darkhax.bookshelf.common.api.entity.villager.trades.VillagerBuys; import net.darkhax.bookshelf.common.api.registry.ContentProvider; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.data.ingredient.AllOfIngredient; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.item.Item; import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.ItemCost; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockBehaviour; public class DebugContentProvider implements ContentProvider { @Override public void defineBlocks(BlockRegistryAdapter registry) { registry.add("test_block", () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE))); registry.addPlaceable("test_placeable", () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK))); } @Override public void defineItems(GameRegistryAdapter registry) { registry.add("test_item", () -> new Item(new Item.Properties())); } @Override public void defineCreativeTabs(CreativeModeTabAdapter registry) { registry.add("test_tab", Items.BOOKSHELF::getDefaultInstance, (params, output) -> { output.accept(Items.BOOKSHELF); BuiltInRegistries.ITEM.keySet().stream().filter(id -> id.getNamespace().equalsIgnoreCase(Constants.MOD_ID)).forEach(id -> output.accept(BuiltInRegistries.ITEM.get(id))); }); } @Override public void defineTrades(VillagerTradeAdapter registry) { registry.addTrade(VillagerProfession.ARMORER, MerchantTier.NOVICE, new VillagerBuys(() -> new ItemCost(Items.BEDROCK, 1), 1, 1, 0, 0)); registry.addCommonWanderingTrade(new VillagerBuys(() -> new ItemCost(Items.BARRIER, 1), 1, 1, 0, 0)); registry.addRareWanderingTrade(new VillagerBuys(() -> new ItemCost(Items.STRUCTURE_VOID, 1), 1, 1, 0, 0)); } @Override public void defineIngredientTypes(IngredientTypeAdapter registry) { registry.add("test_all", AllOfIngredient.CODEC, AllOfIngredient.STREAM); } @Override public String namespace() { return Constants.MOD_ID; } @Override public boolean canLoad() { final boolean canLoad = Services.PLATFORM.isDevelopmentEnvironment(); if (canLoad) { Constants.LOG.warn("Developer mode is enabled! Bookshelf will load its debug content!"); Constants.LOG.warn("Bookshelf's debug content will affect the gameplay experience!"); Constants.LOG.warn("If you are not in a development environment you really should disable developer mode!"); Constants.LOG.warn("If you are a developer you can ignore this message."); } return canLoad; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/BlockTagToItemTagCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.darkhax.bookshelf.common.api.commands.args.TagArgument; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.core.HolderSet; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.function.Function; public class BlockTagToItemTagCommand { public static LiteralArgumentBuilder build(CommandBuildContext context) { final LiteralArgumentBuilder root = Commands.literal("block_to_item_tag").requires(PermissionLevel.GAMEMASTER); root.then(Commands.argument("block_tag", TagArgument.arg(context, Registries.BLOCK)).executes(ctx -> { final TagKey result = TagArgument.get("block_tag", ctx, Registries.BLOCK); final HolderSet.Named tag = BuiltInRegistries.BLOCK.getTag(result).orElseThrow(); final Set items = new HashSet<>(); for (Item item : BuiltInRegistries.ITEM) { if (item instanceof BlockItem blockItem && tag.contains(blockItem.getBlock().builtInRegistryHolder())) { items.add(blockItem); } } ctx.getSource().sendSuccess(() -> TextHelper.copyText(Constants.GSON_PRETTY.toJson(tagJson(items, BuiltInRegistries.ITEM::getKey))), false); return 0; })); return root; } private static JsonObject tagJson(Collection entries, Function idFunc) { final JsonArray array = new JsonArray(); entries.stream().map(entry -> idFunc.apply(entry).toString()).sorted().forEach(array::add); final JsonObject obj = new JsonObject(); obj.add("values", array); return obj; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/DebugCommands.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import net.darkhax.bookshelf.common.api.commands.IEnumCommand; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriptions; import net.darkhax.bookshelf.common.api.util.CommandHelper; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.common.impl.data.loot.modifiers.ILootPoolHooks; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable; import net.minecraft.client.resources.language.I18n; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.Holder; import net.minecraft.core.HolderGetter; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.storage.loot.LootTable; import java.util.Collection; import java.util.Locale; import java.util.Optional; import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicBoolean; public enum DebugCommands implements IEnumCommand { MISSING_TAG_NAMES(DebugCommands::findMissingTagNames), MISSING_BLOCK_DROPS(DebugCommands::findMissingBlockDrops), LOOT_POOL_HASH(DebugCommands::findLootTableHashes), SIMPLE_TABLES(DebugCommands::printTables); private static void printTables(MinecraftServer server, StringJoiner out) { final Collection tableKeys = server.reloadableRegistries().getKeys(Registries.LOOT_TABLE); final RegistryAccess registries = server.reloadableRegistries().get(); for (ResourceLocation tableKey : tableKeys) { final LootTable table = server.reloadableRegistries().getLootTable(ResourceKey.create(Registries.LOOT_TABLE, tableKey)); out.add(tableKey + " = " + LootPoolEntryDescriptions.getUniqueItems(registries, table)); } } private static void findMissingTagNames(MinecraftServer server, StringJoiner out) { server.registryAccess().registries().forEach(entry -> { AtomicBoolean hasLogged = new AtomicBoolean(false); entry.value().getTagNames().forEach(tag -> { if (!tag.location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE) && !I18n.exists(TextHelper.getTagName(tag))) { if (!hasLogged.get()) { hasLogged.set(true); out.add("## " + entry.key().location()); } out.add("\"" + TextHelper.getTagName(tag) + "\": \"\","); } }); if (hasLogged.get()) { out.add(""); } }); } private static void findMissingBlockDrops(MinecraftServer server, StringJoiner out) { final HolderGetter lootTables = server.reloadableRegistries().lookup().lookup(Registries.LOOT_TABLE).orElseThrow(); for (Block block : BuiltInRegistries.BLOCK) { final Optional> result = lootTables.get(block.getLootTable()); if (result.isEmpty()) { final ResourceLocation id = block.getLootTable().location(); out.add(BuiltInRegistries.BLOCK.getKey(block) + " - " + id + " - data/" + id.getNamespace() + "/loot_table/" + id.getPath()); } } } private static void findLootTableHashes(MinecraftServer server, StringJoiner out) { final HolderGetter lootTables = server.reloadableRegistries().lookup().lookup(Registries.LOOT_TABLE).orElseThrow(); for (ResourceLocation table : server.reloadableRegistries().getKeys(Registries.LOOT_TABLE)) { if (table.getPath().startsWith("chests") || table.getPath().startsWith("dispensers") || table.getPath().startsWith("gameplay") || table.getPath().startsWith("pots") || table.getPath().startsWith("spawners")) { out.add("## " + table); lootTables.get(ResourceKey.create(Registries.LOOT_TABLE, table)).ifPresent(val -> { if (val.value() instanceof AccessorLootTable accessor) { for (int index = 0; index < accessor.bookshelf$pools().size(); index++) { if (accessor.bookshelf$pools().get(index) instanceof ILootPoolHooks fingerprintable) { out.add("- " + index + " | " + fingerprintable.bookshelf$getHash()); } } } }); } } } public static LiteralArgumentBuilder build(CommandBuildContext context) { return CommandHelper.buildFromEnum("debug", DebugCommands.class); } private final DebugTask debugTask; DebugCommands(DebugTask task) { this.debugTask = task; } @Override public String getCommandName() { return this.name().toLowerCase(Locale.ROOT); } @Override public int run(CommandContext context) { final StringJoiner joiner = new StringJoiner(System.lineSeparator()); this.debugTask.getDebugOutput(context.getSource().getServer(), joiner); Constants.LOG.warn(joiner.toString()); final String debugInfo = joiner.toString(); if (debugInfo.isBlank()) { context.getSource().sendFailure(Component.translatable("commands.bookshelf.debug.no_info")); return 0; } else if (debugInfo.length() > 10000) { context.getSource().sendFailure(Component.translatable("commands.bookshelf.debug.too_long")); return 0; } else { context.getSource().sendSuccess(() -> TextHelper.setCopyText(Component.translatable("commands.bookshelf.debug.yes_info"), debugInfo), false); return 1; } } @Override public PermissionLevel requiredPermissionLevel() { return PermissionLevel.OWNER; } @FunctionalInterface public interface DebugTask { void getDebugOutput(MinecraftServer server, StringJoiner output); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/EnchantCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.darkhax.bookshelf.common.api.util.CommandHelper; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.ResourceArgument; import net.minecraft.core.Holder; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.enchantment.Enchantment; public class EnchantCommand { public static LiteralArgumentBuilder build(CommandBuildContext context) { final LiteralArgumentBuilder root = Commands.literal("enchant").requires(PermissionLevel.GAMEMASTER); root.then(Commands.argument("enchantment", ResourceArgument.resource(context, Registries.ENCHANTMENT)).then(Commands.argument("level", IntegerArgumentType.integer(0)).executes(EnchantCommand::enchantItem))); root.then(Commands.argument("target", EntityArgument.entity()).then(Commands.argument("enchantment", ResourceArgument.resource(context, Registries.ENCHANTMENT)).then(Commands.argument("level", IntegerArgumentType.integer(0)).executes(EnchantCommand::enchantItem)))); return root; } private static int enchantItem(CommandContext ctx) throws CommandSyntaxException { final Entity target = CommandHelper.getEntityOrSender("target", ctx); final Holder.Reference enchantment = ResourceArgument.getEnchantment(ctx, "enchantment"); final int level = IntegerArgumentType.getInteger(ctx, "level"); if (target instanceof LivingEntity livingTarget) { if (livingTarget.getMainHandItem().isEmpty()) { ctx.getSource().sendFailure(Component.translatable("commands.bookshelf.hand.error.not_air")); return 0; } livingTarget.getMainHandItem().enchant(enchantment, level); return Command.SINGLE_SUCCESS; } return 0; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/FontCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.darkhax.bookshelf.common.api.commands.args.FontArgument; import net.darkhax.bookshelf.common.api.util.CommandHelper; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.darkhax.bookshelf.common.mixin.access.block.AccessorBannerBlockEntity; import net.darkhax.bookshelf.common.mixin.access.block.AccessorBaseContainerBlockEntity; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.MessageArgument; import net.minecraft.commands.arguments.coordinates.BlockPosArgument; import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.BannerBlockEntity; import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.SignBlockEntity; import net.minecraft.world.level.block.entity.SignText; import java.util.function.UnaryOperator; public class FontCommand { public static LiteralArgumentBuilder build() { final LiteralArgumentBuilder font = Commands.literal("font").requires(PermissionLevel.GAMEMASTER); LiteralArgumentBuilder rename = Commands.literal("rename"); rename.then(FontArgument.argument().executes(FontCommand::renameItemWithFont)); rename.then(Commands.argument("target", EntityArgument.entity()).then(FontArgument.argument().executes(FontCommand::renameItemWithFont))); font.then(rename); font.then(Commands.literal("block").then(FontArgument.argument().then(Commands.argument("pos", BlockPosArgument.blockPos()).executes(FontCommand::renameBlockWithFont)))); font.then(Commands.literal("say").then(FontArgument.argument().then(Commands.argument("message", MessageArgument.message()).executes(FontCommand::speakWithFont)))); return font; } private static int speakWithFont(CommandContext context) throws CommandSyntaxException { final ResourceLocation fontId = FontArgument.get(context); final Component inputMessage = TextHelper.applyFont(MessageArgument.getMessage(context, "message"), fontId); final Component txtMessage = Component.translatable("chat.type.announcement", context.getSource().getDisplayName(), inputMessage); context.getSource().getServer().getPlayerList().broadcastSystemMessage(txtMessage, false); return 0; } private static int renameItemWithFont(CommandContext context) throws CommandSyntaxException { final ResourceLocation fontId = FontArgument.get(context); final Entity target = CommandHelper.getEntityOrSender("target", context); if (target instanceof LivingEntity living) { final ItemStack stack = living.getMainHandItem(); if (stack.isEmpty()) { context.getSource().sendFailure(Component.translatable("commands.bookshelf.hand.error.not_air")); return 0; } stack.set(DataComponents.CUSTOM_NAME, TextHelper.applyFont(stack.getHoverName().copy(), fontId)); return 1; } context.getSource().sendFailure(Component.translatable("commands.bookshelf.font.bad_sender")); return 0; } private static int renameBlockWithFont(CommandContext context) throws CommandSyntaxException { final ServerLevel world = context.getSource().getLevel(); final ResourceLocation fontId = FontArgument.get(context); final BlockPos pos = BlockPosArgument.getLoadedBlockPos(context, "pos"); final BlockEntity tile = world.getBlockEntity(pos); if (tile != null && tile.hasLevel()) { switch (tile) { case BaseContainerBlockEntity container when tile instanceof AccessorBaseContainerBlockEntity accessor -> accessor.bookshelf$name(TextHelper.applyFont(container.getName().copy(), fontId)); case SignBlockEntity sign -> { if (sign.getLevel() != null) { sign.updateText(applySignFont(fontId), true); sign.updateText(applySignFont(fontId), false); sign.getLevel().sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3); } } case BannerBlockEntity banner when banner.hasCustomName() && banner instanceof AccessorBannerBlockEntity accessor -> accessor.setName(TextHelper.applyFont(banner.getCustomName(), fontId)); default -> context.getSource().sendFailure(Component.translatable("commands.bookshelf.font.unsupported_block", tile.getBlockState().getBlock().getName())); } } return 1; } private static UnaryOperator applySignFont(ResourceLocation fontId) { return text -> { SignText newText = text; for (int i = 0; i < 4; i++) { newText = newText.setMessage(i, TextHelper.applyFont(text.getMessage(i, false).copy(), fontId)); } return newText; }; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/HandCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.serialization.Codec; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; import net.darkhax.bookshelf.common.api.commands.IEnumCommand; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.util.CommandHelper; import net.darkhax.bookshelf.common.api.util.TextHelper; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import java.util.Comparator; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; import java.util.function.BiFunction; import java.util.function.Function; public enum HandCommand implements IEnumCommand { ID((stack, level) -> TextHelper.copyText(Objects.requireNonNull(level.registryAccess().registryOrThrow(Registries.ITEM).getKey(stack.getItem())).toString())), STRING((stack, level) -> TextHelper.copyText(stack.toString())), INGREDIENT(json(MapCodecs.INGREDIENT.get(), (stack, level) -> Ingredient.of(stack))), STACK_JSON(json(MapCodecs.ITEM_STACK.get(), (stack, level) -> stack)), STACK_NBT(nbt(MapCodecs.ITEM_STACK.get(), (stack, level) -> stack)), COMPONENTS((stack, level) -> { final StringJoiner joiner = new StringJoiner("\n"); stack.getComponents().stream().sorted(Comparator.comparing(r -> r.type().toString())).forEach(component -> { joiner.add(component.type() + " = " + unsafeEncode(Objects.requireNonNull(component.type().codec()), NbtOps.INSTANCE, component.value(), level)); }); return TextHelper.copyText(joiner.toString()); }), TAGS(((stack, level) -> { final StringJoiner joiner = new StringJoiner("\n"); stack.getTags().map(key -> key.location().toString()).sorted().forEach(joiner::add); return TextHelper.copyText(joiner.toString()); })); private final ItemFormat format; HandCommand(ItemFormat format) { this.format = format; } @Override public int run(CommandContext context) { final CommandSourceStack source = context.getSource(); if (source.getEntity() instanceof LivingEntity living) { context.getSource().sendSuccess(() -> getFormattedResults(context.getSource().getLevel(), living.getMainHandItem()), false); } return Command.SINGLE_SUCCESS; } private Component getFormattedResults(ServerLevel level, ItemStack stack) { if (stack.isEmpty()) { return Component.translatable("commands.bookshelf.hand.error.not_air").withStyle(ChatFormatting.RED); } try { return this.format.formatItem(stack, level); } catch (Throwable e) { Constants.LOG.error("Encountered an error when formatting item as {}.", this.name(), e); } return Component.translatable("commands.bookshelf.hand.error.internal").withStyle(ChatFormatting.RED); } private static ItemFormat json(Codec codec, BiFunction mapper) { return fromCodec(JsonOps.INSTANCE, Constants.GSON_PRETTY::toJson, codec, mapper); } private static ItemFormat nbt(Codec codec, BiFunction mapper) { return fromCodec(NbtOps.INSTANCE, Tag::toString, codec, mapper); } private static ItemFormat fromCodec(DynamicOps ops, Function dataFormatter, Codec codec, BiFunction mapper) { return (stack, level) -> { final T value = mapper.apply(stack, level); final D data = codec.encodeStart(level.registryAccess().createSerializationContext(ops), value).getOrThrow(); return TextHelper.copyText(dataFormatter.apply(data)); }; } @SuppressWarnings({"rawtypes", "unchecked"}) private static T unsafeEncode(Codec codec, DynamicOps ops, Object input, ServerLevel level) { return (T) codec.encodeStart(level.registryAccess().createSerializationContext(ops), input).getOrThrow(); } @Override public String getCommandName() { return this.name().toLowerCase(Locale.ROOT); } public static LiteralArgumentBuilder build(CommandBuildContext context) { return CommandHelper.buildFromEnum("hand", HandCommand.class); } interface ItemFormat { Component formatItem(ItemStack stack, ServerLevel level); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/RenameCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.ComponentArgument; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; public class RenameCommand { public static LiteralArgumentBuilder build(CommandBuildContext context) { return Commands.literal("rename").requires(PermissionLevel.GAMEMASTER).then(Commands.argument("new_name", ComponentArgument.textComponent(context)).executes(ctx -> { final Component newName = ComponentArgument.getComponent(ctx, "new_name"); if (ctx.getSource().getEntity() instanceof LivingEntity living && !living.getMainHandItem().isEmpty()) { living.getMainHandItem().set(DataComponents.CUSTOM_NAME, newName); } return Command.SINGLE_SUCCESS; })); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/StructureCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.coordinates.BlockPosArgument; import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.levelgen.structure.Structure; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; public class StructureCommand { public static LiteralArgumentBuilder build() { final LiteralArgumentBuilder root = Commands.literal("structure").requires(PermissionLevel.GAMEMASTER); root.executes(withPosition(StructureCommand::structureAt, ctx -> ctx.getSource().getEntity().blockPosition())); root.then(Commands.argument("position", BlockPosArgument.blockPos()).executes(withPosition(StructureCommand::structureAt, ctx -> BlockPosArgument.getBlockPos(ctx, "position")))); return root; } private static Set getUniqueStructuresAt(CommandContext context, BlockPos pos) { final ServerLevel level = context.getSource().getLevel(); final Registry registry = level.registryAccess().registryOrThrow(Registries.STRUCTURE); return level.structureManager().startsForStructure(new ChunkPos(pos), s -> true).stream().filter(s -> s.getBoundingBox().isInside(pos)).map(s -> registry.getKey(s.getStructure())).collect(Collectors.toSet()); } private static int structureAt(CommandContext context, BlockPos pos) { final Set structures = getUniqueStructuresAt(context, pos); if (structures.isEmpty()) { context.getSource().sendFailure(Component.translatable("commands.bookshelf.structure.error.no_structures")); } else { context.getSource().sendSuccess(() -> Component.translatable("commands.bookshelf.structure.found", structures.size()).append(Component.literal("\n " + structures.stream().map(ResourceLocation::toString).collect(Collectors.joining("\n ")))), false); } return structures.size(); } private static Command withPosition(BiFunction, BlockPos, Integer> cmd, Function, BlockPos> provider) { return ctx -> cmd.apply(ctx, provider.apply(ctx)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/command/TranslateCommand.java ================================================ package net.darkhax.bookshelf.common.impl.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.darkhax.bookshelf.common.api.commands.PermissionLevel; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.network.chat.Component; public class TranslateCommand { public static LiteralArgumentBuilder build(CommandBuildContext context) { return Commands.literal("translate").requires(PermissionLevel.GAMEMASTER).then(Commands.argument("key", StringArgumentType.word()).executes(ctx -> { ctx.getSource().sendSuccess(() -> Component.translatable(StringArgumentType.getString(ctx, "key")), false); return Command.SINGLE_SUCCESS; })); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/And.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import java.util.List; /** * This load condition will test an array of sub-conditions and make sure all of them are met. */ public class And implements ILoadCondition { public static final ResourceLocation TYPE_ID = Constants.id("and"); public static final CachedSupplier TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID)); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList("conditions", And::getConditions)).apply(instance, And::new)); private final List conditions; private And(List conditions) { this.conditions = conditions; } public List getConditions() { return this.conditions; } @Override public boolean allowLoading() { for (ILoadCondition condition : this.conditions) { if (!condition.allowLoading()) { return false; } } return true; } @Override public ConditionType getType() { return TYPE.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/ModLoaded.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import java.util.Set; /** * This load condition will test that an array of mod IDs are all loaded. */ public class ModLoaded implements ILoadCondition { public static final ResourceLocation TYPE_ID = Constants.id("mod_loaded"); public static final CachedSupplier TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID)); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.STRING.getSet("values", ModLoaded::getRequiredMods)).apply(instance, ModLoaded::new)); private final Set requiredMods; private ModLoaded(Set requiredMods) { this.requiredMods = requiredMods; } @Override public boolean allowLoading() { for (String modId : this.requiredMods) { if (!Services.PLATFORM.isModLoaded(modId)) { return false; } } return true; } public Set getRequiredMods() { return this.requiredMods; } @Override public ConditionType getType() { return TYPE.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/Not.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import java.util.List; /** * This load condition will test an array of sub-conditions and make sure none of them are met. */ public class Not implements ILoadCondition { public static final ResourceLocation TYPE_ID = Constants.id("not"); public static final CachedSupplier TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID)); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList("conditions", Not::getConditions)).apply(instance, Not::new)); private final List conditions; private Not(List conditions) { this.conditions = conditions; } public List getConditions() { return this.conditions; } @Override public boolean allowLoading() { for (ILoadCondition condition : this.conditions) { if (condition.allowLoading()) { return false; } } return true; } @Override public ConditionType getType() { return TYPE.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/OnPlatform.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; /** * This load condition will test the current mod loading platform. */ public class OnPlatform implements ILoadCondition { public static final ResourceLocation TYPE_ID = Constants.id("on_platform"); public static final CachedSupplier TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID)); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.STRING.get("platform", OnPlatform::getRequiredPlatform)).apply(instance, OnPlatform::new)); private final String requiredPlatform; private OnPlatform(String requiredPlatform) { this.requiredPlatform = requiredPlatform; } @Override public boolean allowLoading() { return Services.PLATFORM.getName().equalsIgnoreCase(this.getRequiredPlatform()); } public String getRequiredPlatform() { return this.requiredPlatform; } @Override public ConditionType getType() { return TYPE.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/Or.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import java.util.List; /** * This load condition will test an array of sub-conditions and make sure at least one of them are met. */ public class Or implements ILoadCondition { public static final ResourceLocation TYPE_ID = Constants.id("or"); public static final CachedSupplier TYPE = CachedSupplier.cache(() -> LoadConditions.getType(TYPE_ID)); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(LoadConditions.CODEC_HELPER.getList("conditions", Or::getConditions)).apply(instance, Or::new)); private final List conditions; private Or(List conditions) { this.conditions = conditions; } public List getConditions() { return this.conditions; } @Override public boolean allowLoading() { for (ILoadCondition condition : this.conditions) { if (condition.allowLoading()) { return true; } } return false; } @Override public ConditionType getType() { return TYPE.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/conditions/RegistryContains.java ================================================ package net.darkhax.bookshelf.common.impl.data.conditions; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.conditions.ConditionType; import net.darkhax.bookshelf.common.api.data.conditions.ILoadCondition; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceLocation; import java.util.Set; public class RegistryContains implements ILoadCondition { public static final ResourceLocation BLOCK = Constants.id("block_exists"); public static final ResourceLocation ITEM = Constants.id("item_exists"); public static final ResourceLocation ENTITY = Constants.id("entity_exists"); public static final ResourceLocation BLOCK_ENTITY = Constants.id("block_entity_exists"); private final Registry registry; private final Set requiredIds; private final CachedSupplier type; public static MapCodec> of(ResourceLocation typeId, Registry registry) { return RecordCodecBuilder.mapCodec(instance -> instance.group( MapCodecs.RESOURCE_LOCATION.getSet("values", RegistryContains::getRequiredEntries) ).apply(instance, requiredEntries -> new RegistryContains<>(typeId, registry, requiredEntries))); } private RegistryContains(ResourceLocation typeId, Registry registry, Set requiredIds) { this.registry = registry; this.requiredIds = requiredIds; this.type = CachedSupplier.cache(() -> LoadConditions.getType(typeId)); } @Override public boolean allowLoading() { for (ResourceLocation id : this.requiredIds) { if (!this.registry.containsKey(id)) { return false; } } return true; } public Set getRequiredEntries() { return this.requiredIds; } @Override public ConditionType getType() { return this.type.get(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/criterion/item/NamespaceItemPredicate.java ================================================ package net.darkhax.bookshelf.common.impl.data.criterion.item; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.ItemStack; public record NamespaceItemPredicate(String namespace) implements ItemSubPredicate { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group(MapCodecs.STRING.get("namespace", NamespaceItemPredicate::namespace)).apply(instance, NamespaceItemPredicate::new)); @Override public boolean matches(ItemStack stack) { return namespace.equals(BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace()); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/criterion/trigger/AdvancementTrigger.java ================================================ package net.darkhax.bookshelf.common.impl.data.criterion.trigger; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.minecraft.advancements.AdvancementHolder; import net.minecraft.advancements.critereon.ContextAwarePredicate; import net.minecraft.advancements.critereon.EntityPredicate; import net.minecraft.advancements.critereon.SimpleCriterionTrigger; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.Set; public class AdvancementTrigger extends SimpleCriterionTrigger { public static final AdvancementTrigger TRIGGER = new AdvancementTrigger(); private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter(Instance::player), MapCodecs.RESOURCE_LOCATION.getSet("advancements", Instance::advancementIds) ).apply(instance, Instance::new)); @Override @NotNull public Codec codec() { return CODEC; } public void trigger(ServerPlayer player, AdvancementHolder advancement) { this.trigger(player, instance -> instance.advancementIds().contains(advancement.id())); } public record Instance(Optional player, Set advancementIds) implements SimpleCriterionTrigger.SimpleInstance { @Override @NotNull public Optional player() { return this.player; } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/AllOfIngredient.java ================================================ package net.darkhax.bookshelf.common.impl.data.ingredient; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import java.util.List; public class AllOfIngredient implements IngredientLogic { public static final MapCodec CODEC = MapCodecs.flexibleList(Ingredient.CODEC).xmap(AllOfIngredient::new, i -> i.ingredients).fieldOf("ingredients"); public static final StreamCodec STREAM = StreamCodecs.list(StreamCodecs.INGREDIENT_NON_EMPTY).map(AllOfIngredient::new, v -> v.ingredients); private final List ingredients; public AllOfIngredient(List ingredients) { this.ingredients = ingredients; } @Override public boolean test(ItemStack stack) { for (Ingredient ingredient : this.ingredients) { if (!ingredient.test(stack)) { return false; } } return true; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/BlockTagIngredient.java ================================================ package net.darkhax.bookshelf.common.impl.data.ingredient; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.tags.TagKey; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import java.util.ArrayList; import java.util.List; public class BlockTagIngredient implements IngredientLogic { public static final MapCodec CODEC = MapCodecs.flexibleList(TagKey.codec(Registries.BLOCK)).xmap(BlockTagIngredient::new, l -> l.blockTags).fieldOf("tag"); public static final StreamCodec STREAM = StreamCodec.of( (buf, val) -> buf.writeCollection(val.blockTags, (b1, tag) -> { b1.writeResourceLocation(tag.location()); }), buf -> new BlockTagIngredient(buf.readCollection(ArrayList::new, b1 -> TagKey.create(Registries.BLOCK, b1.readResourceLocation()))) ); private final List> blockTags; private List matches; public BlockTagIngredient(List> blockTags) { this.blockTags = blockTags; } @Override public List getAllMatchingStacks() { if (this.matches == null) { this.matches = new ArrayList<>(); for (TagKey tag : blockTags) { for (Holder entry : BuiltInRegistries.BLOCK.getTagOrEmpty(tag)) { final ItemStack stack = new ItemStack(entry.value()); if (!stack.isEmpty()) { this.matches.add(stack); } } } } return this.matches; } @Override public boolean test(ItemStack stack) { if (stack == null || stack.isEmpty()) { return false; } for (ItemStack valid : this.getAllMatchingStacks()) { if (valid.getItem() == stack.getItem()) { return true; } } return false; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/EitherIngredient.java ================================================ package net.darkhax.bookshelf.common.impl.data.ingredient; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import java.util.List; public class EitherIngredient implements IngredientLogic { public static final MapCodec CODEC = MapCodecs.flexibleList(Ingredient.CODEC).xmap(EitherIngredient::new, i -> i.ingredients).fieldOf("ingredients"); public static final StreamCodec STREAM = StreamCodecs.list(StreamCodecs.INGREDIENT_NON_EMPTY).map(EitherIngredient::new, v -> v.ingredients); private final List ingredients; public EitherIngredient(List ingredients) { this.ingredients = ingredients; } @Override public boolean test(ItemStack stack) { for (Ingredient ingredient : this.ingredients) { if (ingredient.test(stack)) { return true; } } return false; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/FalseIngredient.java ================================================ package net.darkhax.bookshelf.common.impl.data.ingredient; import com.google.gson.JsonObject; import com.mojang.serialization.JsonOps; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; public class FalseIngredient implements IngredientLogic { public static final FalseIngredient SINGLETON = new FalseIngredient(); public static final MapCodec CODEC = MapCodec.unit(SINGLETON); public static final StreamCodec STREAM = StreamCodec.unit(SINGLETON); public static final CachedSupplier INSTANCE = CachedSupplier.cache(() -> { final JsonObject obj = new JsonObject(); obj.addProperty("fabric:type", "bookshelf:false"); obj.addProperty("tag", "bookshelf:false"); return Ingredient.CODEC.decode(JsonOps.INSTANCE, obj).getOrThrow().getFirst(); }); @Override public boolean test(ItemStack stack) { return false; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/ingredient/ModIdIngredient.java ================================================ package net.darkhax.bookshelf.common.impl.data.ingredient; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.data.codecs.stream.StreamCodecs; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; import java.util.List; public class ModIdIngredient implements IngredientLogic { public static final MapCodec CODEC = MapCodecs.flexibleList(Codec.STRING).xmap(ModIdIngredient::new, i -> i.modIds).fieldOf("mod"); public static final StreamCodec STREAM = StreamCodecs.list(StreamCodecs.STRING).map(ModIdIngredient::new, i -> i.modIds); private final List modIds; public ModIdIngredient(List modIds) { this.modIds = modIds; } @Override public boolean test(ItemStack stack) { final String owner = BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace(); for (String id : this.modIds) { if (owner.equals(id)) { return true; } } return false; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/entries/LootItemStack.java ================================================ package net.darkhax.bookshelf.common.impl.data.loot.entries; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.darkhax.bookshelf.common.api.data.codecs.map.MapCodecs; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.loot.LootContext; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType; import net.minecraft.world.level.storage.loot.entries.LootPoolSingletonContainer; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; /** * A LootPool entry type that produces copies of predefined ItemStack. */ public class LootItemStack extends LootPoolSingletonContainer { private static final Supplier TYPE = CachedSupplier.of(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE, Constants.id("item_stack")); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(MapCodecs.ITEM_STACK.get("item", LootItemStack::getBaseStack)) .and(singletonFields(instance)) .apply(instance, LootItemStack::new) ); private final ItemStack baseStack; private LootItemStack(ItemStack baseStack, int weight, int quality, List conditions, List functions) { super(weight, quality, conditions, functions); this.baseStack = baseStack; } public ItemStack getBaseStack() { return this.baseStack.copy(); } @Override protected void createItemStack(Consumer consumer, @NotNull LootContext context) { consumer.accept(this.baseStack.copy()); } @NotNull @Override public LootPoolEntryType getType() { return TYPE.get(); } public static LootItemStack of(ItemStack baseStack, int weight) { return new LootItemStack(baseStack, weight, 0, List.of(), List.of()); } public static LootItemStack of(ItemStack baseStack, int weight, int quality, List conditions, List functions) { return new LootItemStack(baseStack, weight, quality, conditions, functions); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/FingerprintCodec.java ================================================ package net.darkhax.bookshelf.common.impl.data.loot.modifiers; import com.google.gson.JsonElement; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; /** * A codec wrapper that adds functionality to compute and set a fingerprint hash for certain objects during decoding. * This is achieved by hashing the JSON representation of the input. *

* This implementation is only intended for the LootPool codec. Loot pools do not have their own identity, so we use * this hash to keep track of their position within a LootTable and to detect if a user has overwritten the input. * * @param The type for the codec to serialize. */ public class FingerprintCodec implements Codec { private final Codec delegate; public FingerprintCodec(Codec delegate) { this.delegate = delegate; } @Override public DataResult> decode(DynamicOps ops, T1 input) { final DataResult> result = this.delegate.decode(ops, input); if (input instanceof JsonElement json && result.error().isEmpty()) { result.result().ifPresent(r -> { if (r.getFirst() instanceof ILootPoolHooks hooks) { hooks.bookshelf$setHash(json.toString().hashCode()); } }); } return result; } @Override public DataResult encode(T input, DynamicOps ops, T1 prefix) { return this.delegate.encode(input, ops, prefix); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/ILootPoolHooks.java ================================================ package net.darkhax.bookshelf.common.impl.data.loot.modifiers; import org.jetbrains.annotations.Nullable; /** * Internal hooks related to loot pools. */ public interface ILootPoolHooks { void bookshelf$setHash(int fingerprint); @Nullable Integer bookshelf$getHash(); default boolean bookshelf$matches(int toMatch) { final Integer hash = this.bookshelf$getHash(); return hash != null && hash == toMatch; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/data/loot/modifiers/LootModificationHandler.java ================================================ package net.darkhax.bookshelf.common.impl.data.loot.modifiers; import net.darkhax.bookshelf.common.api.data.loot.modifiers.LootPoolAddition; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.common.impl.registry.adapter.LootPoolAdditionAdapter; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootPool; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootTable; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Supplier; import java.util.stream.Collectors; /** * Handles collecting loot pool modifications from various mods and then applying those modifications to loot tables. */ public class LootModificationHandler { public static final Supplier HANDLER = CachedSupplier.cache(() -> { final LootModificationHandler handler = new LootModificationHandler(); Services.CONTENT.get().forEach(provider -> provider.defineLootPoolAdditions(new LootPoolAdditionAdapter(provider.namespace(), handler::addPoolEntry))); return handler; }); private final Map>>> newPoolEntries = new HashMap<>(); private void addPoolEntry(ResourceLocation tableId, int poolIndex, int poolHash, LootPoolAddition entry) { // We consolidate entries together here to avoid additional iterations later on. // This also helps us avoid making the list mutable and immutable many times. final Map>> tableEntries = newPoolEntries.computeIfAbsent(tableId, k -> new LinkedHashMap<>()); final Map> indexEntries = tableEntries.computeIfAbsent(poolIndex, k -> new LinkedHashMap<>()); final List hashEntries = indexEntries.computeIfAbsent(poolHash, k -> new LinkedList<>()); hashEntries.add(entry); } public void processLootTable(ResourceLocation tableId, LootTable table) { if (newPoolEntries.containsKey(tableId) && table instanceof AccessorLootTable accessor) { final List pools = accessor.bookshelf$pools(); for (Map.Entry>> indexEntry : newPoolEntries.get(tableId).entrySet()) { for (Map.Entry> hashEntry : indexEntry.getValue().entrySet()) { final LootPool targetPool = findPool(indexEntry.getKey(), hashEntry.getKey(), pools); if (targetPool == null) { Constants.LOG.warn("Could not locate pool {} in table '{}'. The following loot additions will not be applied. {}", hashEntry.getKey(), tableId, hashEntry.getValue().stream().map(a -> a.id().toString()).collect(Collectors.joining(", "))); } else if (targetPool instanceof AccessorLootPool pool) { final List entries = new LinkedList<>(pool.bookshelf$entries()); for (LootPoolAddition addition : hashEntry.getValue()) { entries.add(addition.entry()); Constants.LOG.debug("Added entry `{}` to pool `{}` in table `{}`.", addition.id(), indexEntry.getKey(), tableId); } pool.bookshelf$setEntries(Collections.unmodifiableList(entries)); } } } } } @Nullable private static LootPool findPool(int targetIndex, int targetHash, List pools) { // Check the target first, which will usually be correct. if (targetIndex > -1 && targetIndex < pools.size() + 1) { final LootPool pool = pools.get(targetIndex); if (pool instanceof ILootPoolHooks hooks && hooks.bookshelf$matches(targetHash)) { return pool; } } // If the pool order has been changed we can not rely on the target index, however // the hash can still be used as long as there is only one match. final List matchingHashes = new ArrayList<>(); for (final LootPool pool : pools) { if (pool instanceof ILootPoolHooks hooks && hooks.bookshelf$matches(targetHash)) { matchingHashes.add(pool); } } return matchingHashes.size() == 1 ? matchingHashes.getFirst() : null; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/recipe/RecipeTypeImpl.java ================================================ package net.darkhax.bookshelf.common.impl.recipe; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeType; import org.jetbrains.annotations.NotNull; public record RecipeTypeImpl>(ResourceLocation id) implements RecipeType { @NotNull @Override public String toString() { return this.id.toString(); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockEntityRendererAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import java.util.function.BiConsumer; @SuppressWarnings("rawtypes") public record BlockEntityRendererAdapter(BiConsumer bindFunc) { public void bind(BlockEntityType type, BlockEntityRendererProvider rendererProvider) { this.bindFunc.accept(type, rendererProvider); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockRegistryAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; /** * A registry adapter for the block registry. */ public class BlockRegistryAdapter extends GameRegistryAdapter { public BlockRegistryAdapter(RegistrationContext context, ResourceKey> regKey, BiConsumer, Supplier> registryFunc) { super(context, regKey, registryFunc); } /** * Adds a new block to the block registry and queues up a BlockItem to be registered automatically during item * registration. The item will be registered using the same ID as the block. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param value A supplier that will produce the block to register. * @return A reference to the registry entry. */ public RegistryReference, Block> addPlaceable(String key, Supplier value) { return this.addPlaceable(key, value, block -> new BlockItem(block, new Item.Properties())); } /** * Adds a new block to the block registry and queues up a custom placer item to be registered automatically during * item registry. The item will be registered using the same ID as the block. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param value A supplier that will produce the block to register. * @param placer A factory that produces the placer item. The input block is the block that was registered. * @return A reference to the registry entry. */ public RegistryReference, Block> addPlaceable(String key, Supplier value, Function placer) { final RegistryReference, Block> reference = this.add(key, value); this.context.addPlaceableBlock(reference, placer); return reference; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/BlockRenderTypeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.minecraft.client.renderer.RenderType; import net.minecraft.world.level.block.Block; import java.util.function.BiConsumer; public record BlockRenderTypeAdapter(BiConsumer bindFunc) { public void add(Block block, RenderType type) { this.bindFunc.accept(block, type); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/CommandArgumentAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import com.mojang.brigadier.arguments.ArgumentType; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.resources.ResourceLocation; import java.util.function.BiConsumer; import java.util.function.Supplier; public class CommandArgumentAdapter extends GenericRegistryAdapter> { public CommandArgumentAdapter(RegistrationContext context, BiConsumer>> registryFunc) { super(context, registryFunc); } @SuppressWarnings({"rawtypes", "unchecked"}) public , T extends ArgumentTypeInfo.Template> void add(String id, Class argumentClass, ArgumentTypeInfo info) { this.add(id, new TypeInfo<>(argumentClass, info)); } public record TypeInfo>(Class> argType, ArgumentTypeInfo typeIfo) { } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/CreativeModeTabAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.service.Services; import net.minecraft.core.Registry; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; public class CreativeModeTabAdapter extends GameRegistryAdapter { public CreativeModeTabAdapter(RegistrationContext context, ResourceKey> regKey, BiConsumer, Supplier> registryFunc) { super(context, regKey, registryFunc); } /** * Adds a new creative mode tab to the game. The title will be based on the registry ID of the tab. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param icon An item to display as the icon for the tab. * @param display Generates the items to display in the tab. */ public void add(String key, Supplier icon, CreativeModeTab.DisplayItemsGenerator display) { this.add(key, builder -> { builder.title(Component.translatable("itemGroup." + this.context.namespace() + "." + key)); builder.icon(icon); builder.displayItems(display); }); } /** * Adds a new creative mode tab to the game. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param builderFunc A creative mode tab builder. */ public void add(String key, Consumer builderFunc) { final CreativeModeTab.Builder builder = Services.GAMEPLAY.tabBuilder(); builderFunc.accept(builder); this.add(key, builder.build()); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/IngredientTypeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import java.util.function.BiConsumer; import java.util.function.Supplier; /** * A registry adapter that can register new types of ingredients. */ @SuppressWarnings("rawtypes") public class IngredientTypeAdapter extends GenericRegistryAdapter { public IngredientTypeAdapter(RegistrationContext context, BiConsumer> registryFunc) { super(context, registryFunc); } /** * Adds a new type of ingredient to the game. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param codec A map codec that constructs the ingredient logic from map data like JSON. * @param stream A ByteBuf codec that constructs the ingredient logic from network data. * @param The type of the ingredient logic. * @return A reference to the registry entry. */ public > RegistryReference add(String key, MapCodec codec, StreamCodec stream) { return this.add(key, new IngredientType<>(codec, stream)); } /** * An internal type that holds a map codec and the ByteBuf codec for a custom ingredient type. * * @param codec A codec that reads the ingredient from map data, like JSON. * @param stream A ByteBuf codec that reads the ingredient from network data. * @param The type of the custom ingredient logic. */ public record IngredientType>(MapCodec codec, StreamCodec stream) { } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootDescriptionAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.loot.LootPoolEntryDescriber; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType; import java.util.function.BiConsumer; public record LootDescriptionAdapter(BiConsumer registryFunc) { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootEntryTypeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType; import java.util.function.BiConsumer; import java.util.function.Supplier; public class LootEntryTypeAdapter extends GameRegistryAdapter { public LootEntryTypeAdapter(RegistrationContext context, ResourceKey> registry, BiConsumer, Supplier> registryFunc) { super(context, registry, registryFunc); } public void add(String key, MapCodec codec) { this.add(key, new LootPoolEntryType(codec)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/LootPoolAdditionAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.data.loot.PoolTarget; import net.darkhax.bookshelf.common.api.data.loot.modifiers.LootPoolAddition; import net.darkhax.bookshelf.common.impl.data.loot.entries.LootItemStack; import net.darkhax.bookshelf.common.mixin.access.loot.AccessorLootItem; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import java.util.List; /** * Registers new LootPoolAddition from various mods to be applied by Bookshelf. * * @param owner The ID of the mod registering loot additions. * @param registerFunc The function used to register new additions. */ public record LootPoolAdditionAdapter(String owner, RegisterFunc registerFunc) { public void add(String id, PoolTarget pool, ItemStack item, int weight) { add(id, pool.table(), pool.index(), pool.hash(), item, weight); } public void add(String id, PoolTarget pool, Item item, int weight) { add(id, pool.table(), pool.index(), pool.hash(), item, weight); } public void add(String id, ResourceKey tableId, int poolIndex, int poolHash, ItemStack item, int weight) { add(id, tableId.location(), poolIndex, poolHash, item, weight); } public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, ItemStack item, int weight) { add(id, tableId, poolIndex, poolHash, LootItemStack.of(item, weight)); } public void add(String id, ResourceKey tableId, int poolIndex, int poolHash, Item item, int weight) { add(id, tableId.location(), poolIndex, poolHash, item, weight); } public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, Item item, int weight) { add(id, tableId, poolIndex, poolHash, AccessorLootItem.bookshelf$create(item.builtInRegistryHolder(), weight, 0, List.of(), List.of())); } public void add(String id, PoolTarget pool, LootPoolEntryContainer addition) { add(id, pool.table(), pool.index(), pool.hash(), addition); } public void add(String id, ResourceKey tableId, int poolIndex, int poolHash, LootPoolEntryContainer addition) { add(id, tableId.location(), poolIndex, poolHash, addition); } public void add(String id, ResourceLocation tableId, int poolIndex, int poolHash, LootPoolEntryContainer addition) { registerFunc.register(tableId, poolIndex, poolHash, new LootPoolAddition(id(id), addition)); } private ResourceLocation id(String id) { return ResourceLocation.fromNamespaceAndPath(this.owner, id); } @FunctionalInterface public interface RegisterFunc { void register(ResourceLocation tableId, int poolIndex, int poolHash, LootPoolAddition addition); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/MenuScreenAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; import java.util.function.BiConsumer; @SuppressWarnings("rawtypes") public record MenuScreenAdapter(BiConsumer func) { public > void bind(MenuType type, ScreenFactory factory) { func.accept(type, factory); } @FunctionalInterface public interface ScreenFactory> { U create(T menu, Inventory playerInv, Component title); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/MenuTypeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.AbstractContainerMenu; import java.util.function.BiConsumer; import java.util.function.Supplier; public class MenuTypeAdapter extends GenericRegistryAdapter> { public MenuTypeAdapter(RegistrationContext context, BiConsumer>> registryFunc) { super(context, registryFunc); } public interface ClientMenuFactory { T create(int containerId, Inventory playerInventory); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PacketAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.network.IPacket; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import java.util.function.Consumer; public record PacketAdapter(RegistrationContext context, Consumer> registerFunc) { public IPacket add(IPacket packet) { registerFunc.accept(packet); return packet; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PotPatternAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.entity.DecoratedPotPattern; import java.util.function.BiConsumer; import java.util.function.Supplier; /** * A registry adapter for decorated pot patterns like sherds. */ public final class PotPatternAdapter extends GameRegistryAdapter { public PotPatternAdapter(RegistrationContext context, ResourceKey> regKey, BiConsumer, Supplier> registryFunc) { super(context, regKey, registryFunc); } /** * Adds a new decorated pot pattern to the game registry. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @return A reference to the registry entry. */ public RegistryReference, DecoratedPotPattern> add(String key) { return this.add(key, () -> new DecoratedPotPattern(this.id(key))); } /** * Adds a new decorated pot pattern to the game registry and associates it with an item. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param item The item to associate the pattern with. * @return A reference to the registry entry. */ public RegistryReference, DecoratedPotPattern> addWithItem(String key, Item item) { final RegistryReference, DecoratedPotPattern> pattern = this.add(key); this.context.addPotPatternItem(item, pattern.key()); return pattern; } /** * Adds a new decorated pot pattern to the game registry and associates it with an item. * * @param key The ID to register the value under. This ID only needs to be unique within your namespace. * @param item The item to associate the pattern with. * @return A reference to the registry entry. */ public RegistryReference, DecoratedPotPattern> addWithItem(String key, Supplier item) { return this.addWithItem(key, item.get()); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/PotionBrewAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; public class PotionBrewAdapter { } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/RecipeTypeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.RegistryReference; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.impl.recipe.RecipeTypeImpl; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.crafting.RecipeType; import java.util.function.BiConsumer; import java.util.function.Supplier; public class RecipeTypeAdapter extends GameRegistryAdapter> { public RecipeTypeAdapter(RegistrationContext context, ResourceKey>> regKey, BiConsumer>, Supplier>> registryFunc) { super(context, regKey, registryFunc); } public RegistryReference>, RecipeType> add(String key) { return this.add(key, () -> new RecipeTypeImpl<>(ResourceLocation.fromNamespaceAndPath(context.namespace(), key))); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/SoundEventAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraft.sounds.SoundEvent; import java.util.function.BiConsumer; import java.util.function.Supplier; public class SoundEventAdapter extends GameRegistryAdapter { public SoundEventAdapter(RegistrationContext context, ResourceKey> registryKey, BiConsumer, Supplier> registryFunc) { super(context, registryKey, registryFunc); } public void fixedRange(String id, float range) { this.add(id, SoundEvent.createFixedRangeEvent(this.id(id), range)); } public void variableRange(String id) { this.add(id, SoundEvent.createVariableRangeEvent(this.id(id))); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/registry/adapter/VillagerTradeAdapter.java ================================================ package net.darkhax.bookshelf.common.impl.registry.adapter; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import net.darkhax.bookshelf.common.api.entity.villager.MerchantTier; import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.entity.npc.VillagerTrades; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public final class VillagerTradeAdapter { private final Map> villagerTrades = new HashMap<>(); private final List rareTrades = new ArrayList<>(); private final List commonTrades = new ArrayList<>(); /** * Adds a new villager trade to the game. * * @param profession The profession that offers the trade. * @param tier The tier of that profession that offers the trade. * @param trade The trade to offer. */ public void addTrade(VillagerProfession profession, int tier, VillagerTrades.ItemListing trade) { villagerTrades.computeIfAbsent(profession, p -> ArrayListMultimap.create()).put(tier, trade); } /** * Adds a new villager trade to the game. * * @param profession The profession that offers the trade. * @param tier The tier of that profession that offers the trade. * @param trade The trade to offer. */ public void addTrade(VillagerProfession profession, MerchantTier tier, VillagerTrades.ItemListing trade) { this.addTrade(profession, tier.ordinal() + 1, trade); } /** * Adds a trade to the wandering trader. * * @param trade The trade for the wandering trader to offer. * @param isRare If the trade should be added to the rare or common pool. */ public void addWanderingTrade(VillagerTrades.ItemListing trade, boolean isRare) { (isRare ? rareTrades : commonTrades).add(trade); } /** * Adds a trade to the wandering traders common trades pool. * * @param trade the trade for the wandering trader to offer. */ public void addCommonWanderingTrade(VillagerTrades.ItemListing trade) { this.addWanderingTrade(trade, false); } /** * Adds a trade to the wandering traders rare trades pool. * * @param trade the trade for the wandering trader to offer. */ public void addRareWanderingTrade(VillagerTrades.ItemListing trade) { this.addWanderingTrade(trade, true); } /** * Gets a read-only view of the villager trades to register. Only contains trades added by the content provider. * * @return A read-only map of villager trades to register. */ public Map> getVillagerTrades() { return Collections.unmodifiableMap(this.villagerTrades); } /** * Gets a read-only view of the rare wandering trader trades. Only contains trades added by the content provider. * * @return The rare wandering trader trades. */ public List getRareWanderingTrades() { return Collections.unmodifiableList(this.rareTrades); } /** * Gets a read-only view of the common wandering trader trades. Only contains trades added by the content provider. * * @return the common wandering trader trades. */ public List getCommonWanderingTrades() { return Collections.unmodifiableList(this.commonTrades); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/impl/resources/ExtendedText.java ================================================ package net.darkhax.bookshelf.common.impl.resources; import net.darkhax.bookshelf.common.api.ModEntry; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.DetectedVersion; import net.minecraft.client.Minecraft; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; public class ExtendedText { public static final Supplier INSTANCE = CachedSupplier.cache(ExtendedText::new); private final Map> extendedEntries = new LinkedHashMap<>(); private ExtendedText() { this.register("java.version", () -> getProperty("java.version")); this.register("minecraft.version", DetectedVersion.BUILT_IN::getName); this.register("loader.name", Services.PLATFORM::getName); this.register("player.name", () -> Minecraft.getInstance().getUser().getName()); for (ModEntry mod : Services.PLATFORM.getLoadedMods()) { this.register("mods." + mod.modId() + ".name", mod::name); this.register("mods." + mod.modId() + ".desc", mod::description); this.register("mods." + mod.modId() + ".id", mod::modId); this.register("mods." + mod.modId() + ".version", mod::version); } } public boolean has(String key) { return this.extendedEntries.containsKey(key); } public String get(String key) { return this.extendedEntries.get(key).get(); } private void register(String key, Supplier value) { extendedEntries.put("text.bookshelf.ext." + key, CachedSupplier.cache(value)); } private static String getProperty(String propertyName) { try { return System.getProperty(propertyName); } catch (Exception e) { Constants.LOG.debug("Unable to read property {}", propertyName, e); return "unknown"; } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBannerBlockEntity.java ================================================ package net.darkhax.bookshelf.common.mixin.access.block; import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.entity.BannerBlockEntity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(BannerBlockEntity.class) public interface AccessorBannerBlockEntity { @Accessor("name") void setName(Component name); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBaseContainerBlockEntity.java ================================================ package net.darkhax.bookshelf.common.mixin.access.block; import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(BaseContainerBlockEntity.class) public interface AccessorBaseContainerBlockEntity { @Accessor("name") void bookshelf$name(Component name); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorBlockEntityRenderers.java ================================================ package net.darkhax.bookshelf.common.mixin.access.block; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.world.level.block.entity.BlockEntityType; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(BlockEntityRenderers.class) public interface AccessorBlockEntityRenderers { @Invoker("register") @SuppressWarnings("rawtypes") static void bookshelf$register(BlockEntityType type, BlockEntityRendererProvider renderProvider) { } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/block/AccessorCropBlock.java ================================================ package net.darkhax.bookshelf.common.mixin.access.block; import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.CropBlock; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(CropBlock.class) public interface AccessorCropBlock { @Invoker("getBaseSeedId") ItemLike bookshelf$getSeed(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorFontManager.java ================================================ package net.darkhax.bookshelf.common.mixin.access.client; import net.minecraft.client.gui.font.FontManager; import net.minecraft.client.gui.font.FontSet; import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.Map; @Mixin(FontManager.class) public interface AccessorFontManager { @Accessor("fontSets") Map bookshelf$getFonts(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorItemBlockRenderTypes.java ================================================ package net.darkhax.bookshelf.common.mixin.access.client; import net.minecraft.client.renderer.ItemBlockRenderTypes; import net.minecraft.client.renderer.RenderType; import net.minecraft.world.level.block.Block; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.Map; @Mixin(ItemBlockRenderTypes.class) public interface AccessorItemBlockRenderTypes { @Accessor("TYPE_BY_BLOCK") static Map bookshelf$getBlockTypes() { throw new IllegalStateException("Accessor code not reachable."); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/AccessorMinecraft.java ================================================ package net.darkhax.bookshelf.common.mixin.access.client; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.font.FontManager; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(Minecraft.class) public interface AccessorMinecraft { @Accessor("fontManager") FontManager bookshelf$getFontManager(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/client/gui/AccessorAbstractWidget.java ================================================ package net.darkhax.bookshelf.common.mixin.access.client.gui; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(AbstractWidget.class) public interface AccessorAbstractWidget { @Invoker("renderScrollingString") static void bookshelf$renderScrollingString(GuiGraphics guiGraphics, Font font, Component text, int minX, int minY, int maxX, int maxY, int color) { throw new IllegalStateException("Mixins failed to apply."); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/entity/AccessorEntity.java ================================================ package net.darkhax.bookshelf.common.mixin.access.entity; import net.minecraft.network.chat.HoverEvent; import net.minecraft.world.entity.Entity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Entity.class) public interface AccessorEntity { @Invoker("createHoverEvent") HoverEvent bookshelf$createHoverEvent(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/level/AccessorRecipeManager.java ================================================ package net.darkhax.bookshelf.common.mixin.access.level; import com.google.common.collect.Multimap; import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeType; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(RecipeManager.class) public interface AccessorRecipeManager { @Accessor("byType") Multimap, RecipeHolder> bookshelf$byTypeMap(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorCompositeEntryBase.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.world.level.storage.loot.entries.CompositeEntryBase; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.List; @Mixin(CompositeEntryBase.class) public interface AccessorCompositeEntryBase { @Accessor("children") List bookshelf$children(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorDynamicLoot.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.entries.DynamicLoot; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(DynamicLoot.class) public interface AccessorDynamicLoot { @Accessor("name") ResourceLocation bookshelf$name(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootItem.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.core.Holder; import net.minecraft.world.item.Item; import net.minecraft.world.level.storage.loot.entries.LootItem; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; import java.util.List; @Mixin(LootItem.class) public interface AccessorLootItem { @Invoker("") static LootItem bookshelf$create(Holder item, int weight, int quality, List conditions, List functions) { return null; } @Accessor("item") Holder bookshelf$item(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootPool.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; import net.minecraft.world.level.storage.loot.providers.number.NumberProvider; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.List; @Mixin(LootPool.class) public interface AccessorLootPool { @Accessor("entries") List bookshelf$entries(); @Accessor("entries") @Mutable void bookshelf$setEntries(List entries); @Accessor("conditions") List bookshelf$conditions(); @Accessor("functions") List functions(); @Accessor("rolls") NumberProvider bookshelf$rolls(); @Accessor("bonusRolls") NumberProvider bookshelf$bonusRolls(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootPoolSingletonContainer.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.world.level.storage.loot.entries.LootPoolSingletonContainer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(LootPoolSingletonContainer.class) public interface AccessorLootPoolSingletonContainer { @Accessor("weight") int bookshelf$weight(); @Accessor("quality") int bookshelf$quality(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorLootTable.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import java.util.List; import java.util.Optional; @Mixin(LootTable.class) public interface AccessorLootTable { @Accessor("randomSequence") Optional bookshelf$randomSequence(); @Accessor("pools") List bookshelf$pools(); @Accessor("functions") List bookshelf$functions(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorNestedLootTable.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import com.mojang.datafixers.util.Either; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.entries.NestedLootTable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(NestedLootTable.class) public interface AccessorNestedLootTable { @Accessor("contents") Either, LootTable> bookshelf$contents(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/loot/AccessorTagEntry.java ================================================ package net.darkhax.bookshelf.common.mixin.access.loot; import net.minecraft.tags.TagKey; import net.minecraft.world.item.Item; import net.minecraft.world.level.storage.loot.entries.TagEntry; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(TagEntry.class) public interface AccessorTagEntry { @Accessor("tag") TagKey bookshelf$tag(); @Accessor("expand") boolean bookshelf$expand(); } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/access/particles/AccessSimpleParticleType.java ================================================ package net.darkhax.bookshelf.common.mixin.access.particles; import net.minecraft.core.particles.SimpleParticleType; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(SimpleParticleType.class) public interface AccessSimpleParticleType { @Invoker("") static SimpleParticleType init(boolean overrideLimit) { throw new IllegalStateException("That didn't mix well..."); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/advancement/MixinPlayerAdvancements.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.advancement; import net.darkhax.bookshelf.common.impl.data.criterion.trigger.AdvancementTrigger; import net.minecraft.advancements.AdvancementHolder; import net.minecraft.advancements.AdvancementProgress; import net.minecraft.server.PlayerAdvancements; import net.minecraft.server.level.ServerPlayer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(PlayerAdvancements.class) public abstract class MixinPlayerAdvancements { @Shadow private ServerPlayer player; @Shadow public abstract AdvancementProgress getOrStartProgress(AdvancementHolder advHolder); @Inject(method = "award(Lnet/minecraft/advancements/AdvancementHolder;Ljava/lang/String;)Z", at = @At("RETURN")) private void onAward(AdvancementHolder advancement, String criterion, CallbackInfoReturnable cir) { if (this.getOrStartProgress(advancement).isDone()) { AdvancementTrigger.TRIGGER.trigger(this.player, advancement); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/block/MixinDecoratedPotPatterns.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.block; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.entity.DecoratedPotPattern; import net.minecraft.world.level.block.entity.DecoratedPotPatterns; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(DecoratedPotPatterns.class) public class MixinDecoratedPotPatterns { @Inject(method = "getPatternFromItem", at = @At("TAIL"), cancellable = true) private static void getResourceKey(Item item, CallbackInfoReturnable> cbi) { if (RegistrationContext.POT_PATTERN_ITEMS.containsKey(item)) { cbi.setReturnValue(RegistrationContext.POT_PATTERN_ITEMS.get(item)); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/client/MixinClientPacketListener.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.client; import net.darkhax.bookshelf.common.api.data.ISidedRecipeManager; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.multiplayer.CommonListenerCookie; import net.minecraft.network.Connection; import net.minecraft.world.item.crafting.RecipeManager; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(value = ClientPacketListener.class, priority = 1005) public class MixinClientPacketListener { @Shadow @Final private RecipeManager recipeManager; @Inject(method = "", at = @At("TAIL")) public void onInit(Minecraft mc, Connection connection, CommonListenerCookie cookie, CallbackInfo ci) { if (this.recipeManager instanceof ISidedRecipeManager sided) { sided.bookshelf$setLogicalClient(); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/entity/MixinLightningBolt.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.entity; import com.llamalad7.mixinextras.sugar.Local; import net.darkhax.bookshelf.common.api.block.IBlockHooks; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LightningBolt.class) public class MixinLightningBolt { @Inject(method = "powerLightningRod", at = @At("RETURN")) private void onLightningStrike(CallbackInfo ci, @Local BlockPos strikePos, @Local BlockState strikeState) { final LightningBolt self = (LightningBolt) (Object) this; final Block strikeBlock = strikeState.getBlock(); Direction[] redirections = strikeBlock == Blocks.LIGHTNING_ROD ? IBlockHooks.LIGHTNING_REDIRECTION_FACES : IBlockHooks.NO_LIGHTNING_REDIRECTION_FACES; if (strikeBlock instanceof IBlockHooks extended) { extended.onLightningStrike(strikeState, self.level(), strikePos, self); redirections = extended.redirectLightningStrike(strikeState, self.level(), strikePos); } for (Direction direction : redirections) { final BlockPos indirectPos = strikePos.relative(direction); final BlockState indirectState = self.level().getBlockState(indirectPos); if (indirectState.getBlock() instanceof IBlockHooks extended) { extended.onLightningStrikeIndirect(indirectState, self.level(), indirectPos, self, strikePos); } } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/entity/MixinLivingEntity.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.entity; import net.darkhax.bookshelf.common.api.data.BookshelfTags; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(LivingEntity.class) public abstract class MixinLivingEntity extends Entity { @Shadow protected int lastHurtByPlayerTime; @Shadow private int lastHurtByMobTimestamp; /** * This patch allows mobs killed by Bookshelf's fake player damage to drop EXP and player specific loot. Bookshelf's * fake player damage is not connected to a specific entity instance so the timers responsible for these checks are * not updated otherwise. */ @Inject(method = "hurt", at = @At("HEAD")) private void updateFakePlayerDamageTimes(DamageSource source, float amount, CallbackInfoReturnable callback) { if (!this.level().isClientSide && !this.isInvulnerableTo(source) && source.is(BookshelfTags.FAKE_PLAYER_DAMAGE)) { this.lastHurtByPlayerTime = this.tickCount; this.lastHurtByMobTimestamp = this.tickCount; } } private MixinLivingEntity() { super(null, null); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/item/MixinCreativeModeTab.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.item; import net.darkhax.bookshelf.common.api.item.IItemHooks; import net.darkhax.bookshelf.common.api.util.DataHelper; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; @Mixin(CreativeModeTab.class) public class MixinCreativeModeTab { @Shadow private Collection displayItems; @Shadow private Set displayItemsSearchTab; @Unique private static final Map> TAG_CACHE = new HashMap<>(); @Unique private static final ResourceLocation OP_ITEMS_ID = ResourceLocation.fromNamespaceAndPath("minecraft", "op_blocks"); @Inject(method = "buildContents(Lnet/minecraft/world/item/CreativeModeTab$ItemDisplayParameters;)V", at = @At("TAIL")) private void buildContents(CreativeModeTab.ItemDisplayParameters parameters, CallbackInfo cbi) { final CreativeModeTab self = (CreativeModeTab) (Object) this; final ResourceLocation id = BuiltInRegistries.CREATIVE_MODE_TAB.getKey(self); if (id != null && (!self.isAlignedRight() || id.equals(OP_ITEMS_ID)) && (!id.equals(OP_ITEMS_ID) || parameters.hasPermissions())) { final TagKey tabTag = TAG_CACHE.computeIfAbsent(id, key -> TagKey.create(Registries.ITEM, Constants.id("creative_tab/" + key.getNamespace() + "/" + key.getPath()))); for (Holder tagEntry : DataHelper.getTagOrEmpty(parameters.holders(), Registries.ITEM, tabTag)) { try { final Item item = tagEntry.value(); if (item instanceof IItemHooks hooks) { hooks.addCreativeTabForms(self, stack -> { displayItems.add(stack); displayItemsSearchTab.add(stack); }); } else { final ItemStack stack = new ItemStack(item); displayItems.add(stack); displayItemsSearchTab.add(stack); } } catch (Exception e) { Constants.LOG.error("Unable to add tag entries to creative tab!", e); } } } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/level/MixinRecipeManager.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.level; import com.google.gson.JsonElement; import net.darkhax.bookshelf.common.api.data.ISidedRecipeManager; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeManager; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Map; @Mixin(RecipeManager.class) public class MixinRecipeManager implements ISidedRecipeManager { @Unique private boolean bookshelf$isClient = false; @Unique private boolean bookshelf$isServer = false; @Inject(method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", at = @At("RETURN")) private void onReload(Map object, ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfo ci) { if (this.bookshelf$isServer) { Constants.SERVER_REVISION++; } } @Inject(method = "replaceRecipes", at = @At("RETURN")) private void onRecipesUpdated(Iterable> recipes, CallbackInfo ci) { if (this.bookshelf$isClient) { Constants.CLIENT_REVISION++; } } @Override public void bookshelf$setLogicalClient() { this.bookshelf$isClient = true; this.bookshelf$isServer = false; } @Override public void bookshelf$setLogicalServer() { this.bookshelf$isServer = true; this.bookshelf$isClient = false; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/level/MixinWalkNodeEvaluator.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.level; import net.darkhax.bookshelf.common.api.block.IBlockHooks; import net.minecraft.core.BlockPos; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.pathfinder.PathType; import net.minecraft.world.level.pathfinder.WalkNodeEvaluator; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; @Mixin(WalkNodeEvaluator.class) public class MixinWalkNodeEvaluator { /** * This patch allows modded blocks to control their own pathfinding type. This is done by implementing * {@link IBlockHooks#getPathfindingType(BlockState, BlockGetter, BlockPos)}. */ @Inject(method = "getPathTypeFromState(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/pathfinder/PathType;", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;"), locals = LocalCapture.CAPTURE_FAILSOFT, cancellable = true) private static void getBlockPathTypeRaw(BlockGetter level, BlockPos pos, CallbackInfoReturnable cbi, BlockState state) { if (state.getBlock() instanceof IBlockHooks hooks) { final PathType customType = hooks.getPathfindingType(state, level, pos); if (customType != null) { cbi.setReturnValue(customType); } } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/locale/MixinClientLanguage.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.locale; import net.darkhax.bookshelf.common.impl.resources.ExtendedText; import net.minecraft.client.resources.language.ClientLanguage; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(ClientLanguage.class) public class MixinClientLanguage { @Inject(method = "getOrDefault", at = @At("HEAD"), cancellable = true) public void getOrDefault(String key, String fallback, CallbackInfoReturnable cbi) { if (ExtendedText.INSTANCE.get().has(key)) { cbi.setReturnValue(ExtendedText.INSTANCE.get().get(key)); } } @Inject(method = "has", at = @At("HEAD"), cancellable = true) public void has(String key, CallbackInfoReturnable cbi) { if (ExtendedText.INSTANCE.get().has(key)) { cbi.setReturnValue(true); } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootDataType.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.loot; import com.google.gson.JsonObject; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.impl.data.loot.modifiers.LootModificationHandler; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.LootDataType; import net.minecraft.world.level.storage.loot.LootTable; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import java.util.Optional; @Mixin(LootDataType.class) public class MixinLootDataType { @Inject(method = "deserialize(Lnet/minecraft/resources/ResourceLocation;Lcom/mojang/serialization/DynamicOps;Ljava/lang/Object;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/DataResult;error()Ljava/util/Optional;"), locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true) private void onDeserialize(ResourceLocation id, DynamicOps ops, Object value, CallbackInfoReturnable> cir, DataResult result) { // Allow bookshelf load conditions to be used on loot tables. if (value instanceof JsonObject obj && !LoadConditions.canLoad(obj)) { cir.setReturnValue(Optional.empty()); return; } // These conditions have been split up because IDEA thinks it will always be false. // This is not the case, and is related to mixin shenanigans. if ((Object) this == LootDataType.TABLE) { if (value instanceof JsonObject && result.error().isEmpty()) { final Object rst = result.result().orElse(null); LootTable table = bookshelf$getLootTable(rst); if (table != null) { LootModificationHandler.HANDLER.get().processLootTable(id, table); } } } } @Nullable @Unique private static LootTable bookshelf$getLootTable(Object rst) { // Under normal circumstances rst is always a LootTable but NeoForge has // patched the code to use Optional instead so we need to // check and resolve those as well. LootTable table = null; if (rst instanceof LootTable lt) { table = lt; } else if (rst instanceof Optional optionalObj && optionalObj.orElse(null) instanceof LootTable lt) { table = lt; } return table; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootItemKilledByPlayerCondition.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.loot; import net.darkhax.bookshelf.common.api.data.BookshelfTags; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.level.storage.loot.LootContext; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.level.storage.loot.predicates.LootItemKilledByPlayerCondition; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(LootItemKilledByPlayerCondition.class) public class MixinLootItemKilledByPlayerCondition { /** * This patch allows mobs that were killed with Bookshelfs fake player damage to satisfy the * minecraft:killed_by_player loot condition. */ @Inject(method = "test(Lnet/minecraft/world/level/storage/loot/LootContext;)Z", at = @At("HEAD"), cancellable = true) public void test(LootContext context, CallbackInfoReturnable callback) { if (context != null && context.hasParam(LootContextParams.DAMAGE_SOURCE)) { final DamageSource source = context.getParam(LootContextParams.DAMAGE_SOURCE); if (source.is(BookshelfTags.FAKE_PLAYER_DAMAGE)) { callback.setReturnValue(true); } } } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/loot/MixinLootPool.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.loot; import com.mojang.serialization.Codec; import net.darkhax.bookshelf.common.impl.data.loot.modifiers.FingerprintCodec; import net.darkhax.bookshelf.common.impl.data.loot.modifiers.ILootPoolHooks; import net.minecraft.world.level.storage.loot.LootPool; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LootPool.class) public class MixinLootPool implements ILootPoolHooks { @Shadow @Final @Mutable public static Codec CODEC; @Unique private Integer bookshelf$fingerprint = null; @Inject(method = "", at = @At("RETURN")) private static void onClassInit(CallbackInfo ci) { CODEC = new FingerprintCodec<>(CODEC); } @Override public void bookshelf$setHash(int fingerprint) { this.bookshelf$fingerprint = fingerprint; } @Override public Integer bookshelf$getHash() { return this.bookshelf$fingerprint; } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/packs/MixinSimpleJsonResourceReloadListener.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.packs; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; import net.minecraft.util.profiling.ProfilerFiller; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Map; @Mixin(SimpleJsonResourceReloadListener.class) public class MixinSimpleJsonResourceReloadListener { /** * This patch introduces load conditions for all JSON based resource loaders. These conditions are independent of * the loader platform allowing them to be used in loader agnostic sourcesets. */ @Inject(method = "prepare(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)Ljava/util/Map;", at = @At("RETURN")) private void prepare(ResourceManager manager, ProfilerFiller profiler, CallbackInfoReturnable> cbi) { cbi.getReturnValue().entrySet().removeIf(entry -> entry.getValue() instanceof JsonObject obj && !LoadConditions.canLoad(obj)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/potions/MixinPotionBrewing.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.potions; import net.darkhax.bookshelf.common.api.service.Services; import net.minecraft.world.item.alchemy.PotionBrewing; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(PotionBrewing.class) public class MixinPotionBrewing { @Inject(method = "addVanillaMixes", at = @At("RETURN")) private static void onBootstrap(PotionBrewing.Builder builder, CallbackInfo ci) { Services.CONTENT.get().forEach(provider -> provider.defineBrews(builder)); } } ================================================ FILE: common/src/main/java/net/darkhax/bookshelf/common/mixin/patch/server/MixinReloadableServerResources.java ================================================ package net.darkhax.bookshelf.common.mixin.patch.server; import net.darkhax.bookshelf.common.api.data.ISidedRecipeManager; import net.minecraft.commands.Commands; import net.minecraft.core.RegistryAccess; import net.minecraft.server.ReloadableServerResources; import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.item.crafting.RecipeManager; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ReloadableServerResources.class) public class MixinReloadableServerResources { @Shadow @Final private RecipeManager recipes; @Inject(method = "", at = @At("RETURN")) private void onInit(RegistryAccess.Frozen registry, FeatureFlagSet features, Commands.CommandSelection commands, int functionLevel, CallbackInfo ci) { if (this.recipes instanceof ISidedRecipeManager sided) { sided.bookshelf$setLogicalServer(); } } } ================================================ FILE: common/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.registry.ContentProvider ================================================ net.darkhax.bookshelf.common.impl.BookshelfContent ================================================ FILE: common/src/main/resources/assets/bookshelf/lang/en_us.json ================================================ { "__formatting": "", "format.bookshelf.right": "%s: %s", "format.bookshelf.center": "%s : %s", "format.bookshelf.left": "%s :%s", "format.bookshelf.spaced": "%s %s", "format.bookshelf.none": "%s%s", "format.bookshelf.unit_rate": "%s/%s", "__commands": "Command Text", "commands.bookshelf.hand.error.not_air": "Item must not be empty or air!", "commands.bookshelf.hand.error.internal": "Error encountered while formatting text. Check logs for more info.", "commands.bookshelf.font.unsupported_block": "Could not apply font to '%s'. This type of block is not supported.", "commands.bookshelf.font.bad_sender": "The font rename command", "commands.bookshelf.debug.no_info": "No debug information available for this command.", "commands.bookshelf.debug.yes_info": "Debug information has been logged to your game log. Click to copy output.", "commands.bookshelf.debug.too_long": "Debug output is too big for chat. Please check your game log instead.", "commands.bookshelf.structure.error.no_structures": "No structures could be found.", "commands.bookshelf.structure.found": "Found %s structure(s)!", "__time_units": "Entries for various time units that may be displayed in game.", "units.bookshelf.tick": "Tick", "units.bookshelf.tick.plural": "Ticks", "units.bookshelf.tick.abbreviated": "t", "units.bookshelf.nanosecond": "Nanosecond", "units.bookshelf.nanosecond.plural": "Nanoseconds", "units.bookshelf.nanosecond.abbreviated": "ns", "units.bookshelf.millisecond": "Millisecond", "units.bookshelf.millisecond.plural": "Milliseconds", "units.bookshelf.millisecond.abbreviated": "ms", "units.bookshelf.second": "Second", "units.bookshelf.second.plural": "Seconds", "units.bookshelf.second.abbreviated": "s", "units.bookshelf.minute": "Minute", "units.bookshelf.minute.plural": "Minutes", "units.bookshelf.minute.abbreviated": "m", "units.bookshelf.hour": "Hour", "units.bookshelf.hour.plural": "Hours", "units.bookshelf.hour.abbreviated": "h", "units.bookshelf.day": "Day", "units.bookshelf.day.plural": "Days", "units.bookshelf.day.abbreviated": "d", "units.bookshelf.week": "Week", "units.bookshelf.week.plural": "Weeks", "units.bookshelf.week.abbreviated": "wk", "units.bookshelf.month": "Month", "units.bookshelf.month.plural": "Months", "units.bookshelf.month.abbreviated": "mo", "units.bookshelf.year": "Year", "units.bookshelf.year.plural": "Years", "units.bookshelf.year.abbreviated": "yr", "__months": "Names of the months", "month.bookshelf.january": "January", "month.bookshelf.february": "February", "month.bookshelf.march": "March", "month.bookshelf.april": "April", "month.bookshelf.may": "May", "month.bookshelf.june": "June", "month.bookshelf.july": "July", "month.bookshelf.august": "August", "month.bookshelf.september": "September", "month.bookshelf.october": "October", "month.bookshelf.november": "November", "month.bookshelf.december": "December", "__days": "Names of the days", "day.bookshelf.sunday": "Sunday", "day.bookshelf.monday": "Monday", "day.bookshelf.tuesday": "Tuesday", "day.bookshelf.wednesday": "Wednesday", "day.bookshelf.thursday": "Thursday", "day.bookshelf.friday": "Friday", "day.bookshelf.saturday": "Saturday", "__moon_phases": "The names of different moon phases that appear in game.", "moon.phase.full": "Full Moon", "moon.phase.waxing.gibbous": "Waxing Gibbous", "moon.phase.first.quarter": "First Quarter", "moon.phase.waxing.crescent": "Waxing Crescent", "moon.phase.new": "New Moon", "moon.phase.waning.crescent": "Waning Crescent", "moon.phase.last.quarter": "Last Quarter", "moon.phase.waning.gibbous": "Waning Gibbous", "__fonts": "Unofficial language entries for the vanilla text fonts.", "font.minecraft.default": "Default", "font.minecraft.default.desc": "The standard font in Minecraft.", "font.minecraft.default.preview": "The quick brown fox jumps over the lazy dog.", "font.minecraft.alt": "Standard Galactic Alphabet", "font.minecraft.alt.desc": "A rune based font associated with enchanting and magic.", "font.minecraft.alt.preview": "Majik fox cub solved the waspy dragons quiz", "font.minecraft.illageralt": "Illager Alphabet", "font.minecraft.illageralt.desc": "A mysterious font used by the illagers.", "font.minecraft.illageralt.preview": "Grumpy wizards make a toxic brew for the jovial queen.", "font.minecraft.uniform": "Uniform", "font.minecraft.uniform.desc": "A plain font that is not stylized.", "font.minecraft.uniform.preview": "The quick brown fox jumps over the lazy dog.", "__Tags": "Tag Names for Recipe Viewers", "tag.item.bookshelf.creative_tab.minecraft.colored_blocks": "Colored Blocks Tab", "tag.item.bookshelf.creative_tab.minecraft.food_and_drinks": "Food and Drinks Tab", "tag.item.bookshelf.creative_tab.minecraft.spawn_eggs": "Spawn Eggs Tab", "tag.item.bookshelf.creative_tab.minecraft.redstone_blocks": "Redstone Tab", "tag.item.bookshelf.creative_tab.minecraft.op_blocks": "OP/Admin Tab", "tag.item.bookshelf.creative_tab.minecraft.combat": "Combat Tab", "tag.item.bookshelf.creative_tab.minecraft.building_blocks": "Building Blocks Tab", "tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities": "Tools & Utilities Tab", "tag.item.bookshelf.creative_tab.minecraft.natural_blocks": "Natural Blocks Tab", "tag.item.bookshelf.creative_tab.minecraft.functional_blocks": "Functional Blocks Tab", "tag.item.bookshelf.creative_tab.minecraft.ingredients": "Ingredients Tab", "tooltips.bookshelf.loot.unknown": "Unknown Drop", "tooltips.bookshelf.loot.unknown.desc": "Some drops can not be displayed right now.", "tooltips.bookshelf.loot.empty": "Empty Drop", "tooltips.bookshelf.loot.empty.desc": "It's possible for nothing to drop!", "tooltips.bookshelf.loot.dynamic": "Dynamic Drop", "tooltips.bookshelf.loot.dynamic.desc": "Some drops depend on the context.", "gui.jei.category.loot.name": "Loot Table", "table.minecraft.archaeology.desert_pyramid.name": "Desert Pyramid", "table.minecraft.archaeology.desert_well.name": "Desert Well", "table.minecraft.archaeology.ocean_ruin_cold.name": "Ocean Ruin - Cold", "table.minecraft.archaeology.ocean_ruin_warm.name": "Ocean Ruin - Warm", "table.minecraft.archaeology.trail_ruins_common.name": "Trail Ruins - Common", "table.minecraft.archaeology.trail_ruins_rare.name": "Trail Ruins - Rare", "table.minecraft.chests.abandoned_mineshaft.name": "Abandoned Mineshaft", "table.minecraft.chests.ancient_city.name": "Ancient City", "table.minecraft.chests.ancient_city_ice_box.name": "Ancient City Ice Box", "table.minecraft.chests.bastion_bridge.name": "Bastion - Bridge", "table.minecraft.chests.bastion_hoglin_stable.name": "Bastion - Hoglin Stable", "table.minecraft.chests.bastion_other.name": "Bastion - Other", "table.minecraft.chests.bastion_treasure.name": "Bastion - Treasure", "table.minecraft.chests.buried_treasure.name": "Buried Treasure", "table.minecraft.chests.desert_pyramid.name": "Desert Pyramid", "table.minecraft.chests.end_city_treasure.name": "End City Treasure", "table.minecraft.chests.igloo_chest.name": "Igloo Chest", "table.minecraft.chests.jungle_temple.name": "Jungle Temple - Chest", "table.minecraft.chests.jungle_temple_dispenser.name": "Jungle Temple - Dispenser", "table.minecraft.chests.nether_bridge.name": "Nether Bridge", "table.minecraft.chests.pillager_outpost.name": "Pillager Outpost", "table.minecraft.chests.ruined_portal.name": "Ruined Portal", "table.minecraft.chests.shipwreck_map.name": "Shipwreck Map", "table.minecraft.chests.shipwreck_supply.name": "Shipwreck - Supply", "table.minecraft.chests.shipwreck_treasure.name": "Shipwreck - Treasure", "table.minecraft.chests.simple_dungeon.name": "Dungeon", "table.minecraft.chests.spawn_bonus_chest.name": "Spawn Bonus Chest", "table.minecraft.chests.stronghold_corridor.name": "Stronghold - Corridor", "table.minecraft.chests.stronghold_crossing.name": "Stronghold - Crossing", "table.minecraft.chests.stronghold_library.name": "Stronghold - Library", "table.minecraft.chests.trial_chambers.corridor.name": "Trial Chamber - Corridor", "table.minecraft.chests.trial_chambers.entrance.name": "Trial Chamber - Entrance", "table.minecraft.chests.trial_chambers.intersection.name": "Trial Chamber - Intersection", "table.minecraft.chests.trial_chambers.intersection_barrel.name": "Trial Chamber - Intersection Barrel", "table.minecraft.chests.trial_chambers.reward.name": "Trial Chamber - Reward", "table.minecraft.chests.trial_chambers.reward_common.name": "Trial Chamber - Common Reward", "table.minecraft.chests.trial_chambers.reward_ominous.name": "Trial Chamber - Ominous Reward", "table.minecraft.chests.trial_chambers.reward_ominous_common.name": "Trial Chamber - Ominous Reward - Common", "table.minecraft.chests.trial_chambers.reward_ominous_rare.name": "Trial Chamber - Ominous Reward - Rare", "table.minecraft.chests.trial_chambers.reward_ominous_unique.name": "Trial Chamber - Ominous Reward - Unique", "table.minecraft.chests.trial_chambers.reward_rare.name": "Trial Chamber - Rare Reward", "table.minecraft.chests.trial_chambers.reward_unique.name": "Trial Chamber - Unique Reward", "table.minecraft.chests.trial_chambers.supply.name": "Trial Chamber - Supply", "table.minecraft.chests.underwater_ruin_big.name": "Underwater Ruin - Big", "table.minecraft.chests.underwater_ruin_small.name": "Underwater Ruin - Small", "table.minecraft.chests.village.village_armorer.name": "Village - Armorer", "table.minecraft.chests.village.village_butcher.name": "Village - Butcher", "table.minecraft.chests.village.village_cartographer.name": "Village - Cartographer", "table.minecraft.chests.village.village_desert_house.name": "Village - Desert House", "table.minecraft.chests.village.village_fisher.name": "Village - Fisher", "table.minecraft.chests.village.village_fletcher.name": "Village - Fletcher", "table.minecraft.chests.village.village_mason.name": "Village - Mason", "table.minecraft.chests.village.village_plains_house.name": "Village - Plains House", "table.minecraft.chests.village.village_savanna_house.name": "Village - Savanna House", "table.minecraft.chests.village.village_shepherd.name": "Village - Shepherd", "table.minecraft.chests.village.village_snowy_house.name": "Village - Snowy House", "table.minecraft.chests.village.village_taiga_house.name": "Village - Taiga House", "table.minecraft.chests.village.village_tannery.name": "Village - Tannery", "table.minecraft.chests.village.village_temple.name": "Village - Temple", "table.minecraft.chests.village.village_toolsmith.name": "Village - Toolsmith", "table.minecraft.chests.village.village_weaponsmith.name": "Village - Weaponsmith", "table.minecraft.chests.woodland_mansion.name": "Woodland Mansion", "table.minecraft.dispensers.trial_chambers.chamber.name": "Trial Chamber - Chamber", "table.minecraft.dispensers.trial_chambers.corridor.name": "Trial Chamber - Corridor", "table.minecraft.dispensers.trial_chambers.water.name": "Trial Chamber - Water", "table.minecraft.empty.name": "Empty", "table.minecraft.equipment.trial_chamber.name": "Trial Chamber", "table.minecraft.equipment.trial_chamber_melee.name": "Trial Chamber - Melee", "table.minecraft.equipment.trial_chamber_ranged.name": "Trial Chamber - Ranged", "table.minecraft.gameplay.cat_morning_gift.name": "Cat - Morning Gift", "table.minecraft.gameplay.fishing.fish.name": "Fishing - Fish", "table.minecraft.gameplay.fishing.junk.name": "Fishing - Junk", "table.minecraft.gameplay.fishing.name": "Fishing", "table.minecraft.gameplay.fishing.treasure.name": "Fishing - Treasure", "table.minecraft.gameplay.hero_of_the_village.armorer_gift.name": "Hero Of The Village - Armorer", "table.minecraft.gameplay.hero_of_the_village.butcher_gift.name": "Hero Of The Village - Butcher", "table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name": "Hero Of The Village - Cartographer", "table.minecraft.gameplay.hero_of_the_village.cleric_gift.name": "Hero Of The Village - Cleric", "table.minecraft.gameplay.hero_of_the_village.farmer_gift.name": "Hero Of The Village - Farmer", "table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name": "Hero Of The Village - Fisherman", "table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name": "Hero Of The Village - Fletcher", "table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name": "Hero Of The Village - Leatherworker", "table.minecraft.gameplay.hero_of_the_village.librarian_gift.name": "Hero Of The Village - Librarian", "table.minecraft.gameplay.hero_of_the_village.mason_gift.name": "Hero Of The Village - Mason", "table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name": "Hero Of The Village - Shepherd", "table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name": "Hero Of The Village - Toolsmith", "table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name": "Hero Of The Village - Weaponsmith", "table.minecraft.gameplay.panda_sneeze.name": "Panda Sneeze", "table.minecraft.gameplay.piglin_bartering.name": "Piglin Bartering", "table.minecraft.gameplay.sniffer_digging.name": "Sniffer Digging", "table.minecraft.pots.trial_chambers.corridor.name": "Trial Chamber - Corridor Pot", "table.minecraft.shearing.bogged.name": "Bogged - Shearing", "table.minecraft.spawners.ominous.trial_chamber.consumables.name": "Trial Chamber - Ominous Consumables", "table.minecraft.spawners.ominous.trial_chamber.key.name": "Trial Chamber - Ominous Key", "table.minecraft.spawners.trial_chamber.consumables.name": "Trial Chamber - Consumables", "table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name": "Trial Chamber - Ominous Drops", "table.minecraft.spawners.trial_chamber.key.name": "Trial Chamber - Key", "table.builtin.block_drops": "Block - %s" } ================================================ FILE: common/src/main/resources/assets/bookshelf/lang/es_ar.json ================================================ { "__formatting": "", "format.bookshelf.right": "%s: %s", "format.bookshelf.center": "%s : %s", "format.bookshelf.left": "%s :%s", "format.bookshelf.spaced": "%s %s", "format.bookshelf.none": "%s%s", "format.bookshelf.unit_rate": "%s/%s", "__commands": "Texto de Comandos", "commands.bookshelf.hand.error.not_air": "¡El ítem no debe estar vacío o ser aire!", "commands.bookshelf.hand.error.internal": "Error encontrado al formatear el texto. Revisá los registros para más información.", "commands.bookshelf.font.unsupported_block": "No se pudo aplicar la fuente a '%s'. Este tipo de bloque no es compatible.", "commands.bookshelf.font.bad_sender": "El comando para renombrar la fuente", "commands.bookshelf.debug.no_info": "No hay información de depuración disponible para este comando.", "commands.bookshelf.debug.yes_info": "La información de depuración se registró en tu archivo de juego. Hacé clic para copiar la salida.", "commands.bookshelf.debug.too_long": "La salida de depuración es demasiado grande para el chat. Por favor, revisá tu archivo de juego en su lugar.", "commands.bookshelf.structure.error.no_structures": "No se encontraron estructuras.", "commands.bookshelf.structure.found": "¡Se encontraron %s estructura(s)!", "__time_units": "Entradas para varias unidades de tiempo que pueden mostrarse en el juego.", "units.bookshelf.tick": "Tick", "units.bookshelf.tick.plural": "Ticks", "units.bookshelf.tick.abbreviated": "t", "units.bookshelf.nanosecond": "Nanosegundo", "units.bookshelf.nanosecond.plural": "Nanosegundos", "units.bookshelf.nanosecond.abbreviated": "ns", "units.bookshelf.millisecond": "Milisegundo", "units.bookshelf.millisecond.plural": "Milisegundos", "units.bookshelf.millisecond.abbreviated": "ms", "units.bookshelf.second": "Segundo", "units.bookshelf.second.plural": "Segundos", "units.bookshelf.second.abbreviated": "s", "units.bookshelf.minute": "Minuto", "units.bookshelf.minute.plural": "Minutos", "units.bookshelf.minute.abbreviated": "m", "units.bookshelf.hour": "Hora", "units.bookshelf.hour.plural": "Horas", "units.bookshelf.hour.abbreviated": "h", "units.bookshelf.day": "Día", "units.bookshelf.day.plural": "Días", "units.bookshelf.day.abbreviated": "d", "units.bookshelf.week": "Semana", "units.bookshelf.week.plural": "Semanas", "units.bookshelf.week.abbreviated": "sem", "units.bookshelf.month": "Mes", "units.bookshelf.month.plural": "Meses", "units.bookshelf.month.abbreviated": "mes", "units.bookshelf.year": "Año", "units.bookshelf.year.plural": "Años", "units.bookshelf.year.abbreviated": "año", "__months": "Nombres de los meses", "month.bookshelf.january": "Enero", "month.bookshelf.february": "Febrero", "month.bookshelf.march": "Marzo", "month.bookshelf.april": "Abril", "month.bookshelf.may": "Mayo", "month.bookshelf.june": "Junio", "month.bookshelf.july": "Julio", "month.bookshelf.august": "Agosto", "month.bookshelf.september": "Septiembre", "month.bookshelf.october": "Octubre", "month.bookshelf.november": "Noviembre", "month.bookshelf.december": "Diciembre", "__days": "Nombres de los días", "day.bookshelf.sunday": "Domingo", "day.bookshelf.monday": "Lunes", "day.bookshelf.tuesday": "Martes", "day.bookshelf.wednesday": "Miércoles", "day.bookshelf.thursday": "Jueves", "day.bookshelf.friday": "Viernes", "day.bookshelf.saturday": "Sábado", "__moon_phases": "Los nombres de las diferentes fases lunares que aparecen en el juego.", "moon.phase.full": "Luna Llena", "moon.phase.waxing.gibbous": "Gibosa Creciente", "moon.phase.first.quarter": "Cuarto Creciente", "moon.phase.waxing.crescent": "Luna Creciente", "moon.phase.new": "Luna Nueva", "moon.phase.waning.crescent": "Luna Menguante", "moon.phase.last.quarter": "Cuarto Menguante", "moon.phase.waning.gibbous": "Gibosa Menguante", "__fonts": "Entradas de idioma no oficiales para las fuentes de texto vanilla.", "font.minecraft.default": "Predeterminada", "font.minecraft.default.desc": "La fuente estándar en Minecraft.", "font.minecraft.default.preview": "El rápido zorro marrón salta sobre el perro perezoso.", "font.minecraft.alt": "Alfabeto Galáctico Estándar", "font.minecraft.alt.desc": "Una fuente basada en runas asociada con encantamientos y magia.", "font.minecraft.alt.preview": "El cachorro de zorro mágico resolvió el acertijo de los dragones avispa", "font.minecraft.illageralt": "Alfabeto Illager", "font.minecraft.illageralt.desc": "Una fuente misteriosa usada por los illagers.", "font.minecraft.illageralt.preview": "Magos gruñones hacen un brebaje tóxico para la reina jovial.", "font.minecraft.uniform": "Uniforme", "font.minecraft.uniform.desc": "Una fuente simple sin estilo.", "font.minecraft.uniform.preview": "El rápido zorro marrón salta sobre el perro perezoso.", "__Tags": "Nombres de Etiquetas para Visores de Recetas", "tag.item.bookshelf.creative_tab.minecraft.colored_blocks": "Pestaña Bloques de Colores", "tag.item.bookshelf.creative_tab.minecraft.food_and_drinks": "Pestaña Comida y Bebidas", "tag.item.bookshelf.creative_tab.minecraft.spawn_eggs": "Pestaña Huevos de Aparición", "tag.item.bookshelf.creative_tab.minecraft.redstone_blocks": "Pestaña Redstone", "tag.item.bookshelf.creative_tab.minecraft.op_blocks": "Pestaña OP/Admin", "tag.item.bookshelf.creative_tab.minecraft.combat": "Pestaña Combate", "tag.item.bookshelf.creative_tab.minecraft.building_blocks": "Pestaña Bloques de Construcción", "tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities": "Pestaña Herramientas y Utilitarios", "tag.item.bookshelf.creative_tab.minecraft.natural_blocks": "Pestaña Bloques Naturales", "tag.item.bookshelf.creative_tab.minecraft.functional_blocks": "Pestaña Bloques Funcionales", "tag.item.bookshelf.creative_tab.minecraft.ingredients": "Pestaña Ingredientes", "tooltips.bookshelf.loot.unknown": "Botín Desconocido", "tooltips.bookshelf.loot.unknown.desc": "Algunos botines no se pueden mostrar en este momento.", "tooltips.bookshelf.loot.empty": "Botín Vacío", "tooltips.bookshelf.loot.empty.desc": "¡Es posible que no caiga nada!", "tooltips.bookshelf.loot.dynamic": "Botín Dinámico", "tooltips.bookshelf.loot.dynamic.desc": "Algunos botines dependen del contexto.", "gui.jei.category.loot.name": "Tabla de Botín", "table.minecraft.archaeology.desert_pyramid.name": "Pirámide del Desierto", "table.minecraft.archaeology.desert_well.name": "Pozo del Desierto", "table.minecraft.archaeology.ocean_ruin_cold.name": "Ruina Oceánica - Fría", "table.minecraft.archaeology.ocean_ruin_warm.name": "Ruina Oceánica - Cálida", "table.minecraft.archaeology.trail_ruins_common.name": "Ruinas del Sendero - Común", "table.minecraft.archaeology.trail_ruins_rare.name": "Ruinas del Sendero - Raro", "table.minecraft.chests.abandoned_mineshaft.name": "Mina Abandonada", "table.minecraft.chests.ancient_city.name": "Ciudad Antigua", "table.minecraft.chests.ancient_city_ice_box.name": "Ciudad Antigua - Caja de Hielo", "table.minecraft.chests.bastion_bridge.name": "Bastión - Puente", "table.minecraft.chests.bastion_hoglin_stable.name": "Bastión - Establo de Hoglins", "table.minecraft.chests.bastion_other.name": "Bastión - Otro", "table.minecraft.chests.bastion_treasure.name": "Bastión - Tesoro", "table.minecraft.chests.buried_treasure.name": "Tesoro Enterrado", "table.minecraft.chests.desert_pyramid.name": "Pirámide del Desierto", "table.minecraft.chests.end_city_treasure.name": "Tesoro de Ciudad del End", "table.minecraft.chests.igloo_chest.name": "Cofre de Iglú", "table.minecraft.chests.jungle_temple.name": "Templo de la Jungla - Cofre", "table.minecraft.chests.jungle_temple_dispenser.name": "Templo de la Jungla - Dispensador", "table.minecraft.chests.nether_bridge.name": "Puente del Nether", "table.minecraft.chests.pillager_outpost.name": "Puesto de Saqueadores", "table.minecraft.chests.ruined_portal.name": "Portal en Ruinas", "table.minecraft.chests.shipwreck_map.name": "Mapa de Naufragio", "table.minecraft.chests.shipwreck_supply.name": "Naufragio - Suministros", "table.minecraft.chests.shipwreck_treasure.name": "Naufragio - Tesoro", "table.minecraft.chests.simple_dungeon.name": "Mazmorra", "table.minecraft.chests.spawn_bonus_chest.name": "Cofre de Bonificación Inicial", "table.minecraft.chests.stronghold_corridor.name": "Fortaleza - Corredor", "table.minecraft.chests.stronghold_crossing.name": "Fortaleza - Cruce", "table.minecraft.chests.stronghold_library.name": "Fortaleza - Biblioteca", "table.minecraft.chests.trial_chambers.corridor.name": "Cámara de Desafío - Corredor", "table.minecraft.chests.trial_chambers.entrance.name": "Cámara de Desafío - Entrada", "table.minecraft.chests.trial_chambers.intersection.name": "Cámara de Desafío - Intersección", "table.minecraft.chests.trial_chambers.intersection_barrel.name": "Cámara de Desafío - Barril de Intersección", "table.minecraft.chests.trial_chambers.reward.name": "Cámara de Desafío - Recompensa", "table.minecraft.chests.trial_chambers.reward_common.name": "Cámara de Desafío - Recompensa Común", "table.minecraft.chests.trial_chambers.reward_ominous.name": "Cámara de Desafío - Recompensa Ominosa", "table.minecraft.chests.trial_chambers.reward_ominous_common.name": "Cámara de Desafío - Recompensa Ominosa - Común", "table.minecraft.chests.trial_chambers.reward_ominous_rare.name": "Cámara de Desafío - Recompensa Ominosa - Rara", "table.minecraft.chests.trial_chambers.reward_ominous_unique.name": "Cámara de Desafío - Recompensa Ominosa - Única", "table.minecraft.chests.trial_chambers.reward_rare.name": "Cámara de Desafío - Recompensa Rara", "table.minecraft.chests.trial_chambers.reward_unique.name": "Cámara de Desafío - Recompensa Única", "table.minecraft.chests.trial_chambers.supply.name": "Cámara de Desafío - Suministros", "table.minecraft.chests.underwater_ruin_big.name": "Ruina Subacuática - Grande", "table.minecraft.chests.underwater_ruin_small.name": "Ruina Subacuática - Pequeña", "table.minecraft.chests.village.village_armorer.name": "Aldea - Herrero de Armaduras", "table.minecraft.chests.village.village_butcher.name": "Aldea - Carnicero", "table.minecraft.chests.village.village_cartographer.name": "Aldea - Cartógrafo", "table.minecraft.chests.village.village_desert_house.name": "Aldea - Casa del Desierto", "table.minecraft.chests.village.village_fisher.name": "Aldea - Pescador", "table.minecraft.chests.village.village_fletcher.name": "Aldea - Flechero", "table.minecraft.chests.village.village_mason.name": "Aldea - Albañil", "table.minecraft.chests.village.village_plains_house.name": "Aldea - Casa de la Llanura", "table.minecraft.chests.village.village_savanna_house.name": "Aldea - Casa de la Sabana", "table.minecraft.chests.village.village_shepherd.name": "Aldea - Pastor", "table.minecraft.chests.village.village_snowy_house.name": "Aldea - Casa Nevada", "table.minecraft.chests.village.village_taiga_house.name": "Aldea - Casa de la Taiga", "table.minecraft.chests.village.village_tannery.name": "Aldea - Curtidor", "table.minecraft.chests.village.village_temple.name": "Aldea - Templo", "table.minecraft.chests.village.village_toolsmith.name": "Aldea - Herrero de Herramientas", "table.minecraft.chests.village.village_weaponsmith.name": "Aldea - Herrero de Armas", "table.minecraft.chests.woodland_mansion.name": "Mansión del Bosque", "table.minecraft.dispensers.trial_chambers.chamber.name": "Cámara de Desafío - Cámara", "table.minecraft.dispensers.trial_chambers.corridor.name": "Cámara de Desafío - Corredor", "table.minecraft.dispensers.trial_chambers.water.name": "Cámara de Desafío - Agua", "table.minecraft.empty.name": "Vacío", "table.minecraft.equipment.trial_chamber.name": "Cámara de Desafío", "table.minecraft.equipment.trial_chamber_melee.name": "Cámara de Desafío - Cuerpo a Cuerpo", "table.minecraft.equipment.trial_chamber_ranged.name": "Cámara de Desafío - A Distancia", "table.minecraft.gameplay.cat_morning_gift.name": "Gato - Regalo Matutino", "table.minecraft.gameplay.fishing.fish.name": "Pesca - Pescado", "table.minecraft.gameplay.fishing.junk.name": "Pesca - Basura", "table.minecraft.gameplay.fishing.name": "Pesca", "table.minecraft.gameplay.fishing.treasure.name": "Pesca - Tesoro", "table.minecraft.gameplay.hero_of_the_village.armorer_gift.name": "Héroe de la Aldea - Herrero de Armaduras", "table.minecraft.gameplay.hero_of_the_village.butcher_gift.name": "Héroe de la Aldea - Carnicero", "table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name": "Héroe de la Aldea - Cartógrafo", "table.minecraft.gameplay.hero_of_the_village.cleric_gift.name": "Héroe de la Aldea - Clérigo", "table.minecraft.gameplay.hero_of_the_village.farmer_gift.name": "Héroe de la Aldea - Granjero", "table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name": "Héroe de la Aldea - Pescador", "table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name": "Héroe de la Aldea - Flechero", "table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name": "Héroe de la Aldea - Peletero", "table.minecraft.gameplay.hero_of_the_village.librarian_gift.name": "Héroe de la Aldea - Bibliotecario", "table.minecraft.gameplay.hero_of_the_village.mason_gift.name": "Héroe de la Aldea - Albañil", "table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name": "Héroe de la Aldea - Pastor", "table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name": "Héroe de la Aldea - Herrero de Herramientas", "table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name": "Héroe de la Aldea - Herrero de Armas", "table.minecraft.gameplay.panda_sneeze.name": "Estornudo de Panda", "table.minecraft.gameplay.piglin_bartering.name": "Intercambio con Piglins", "table.minecraft.gameplay.sniffer_digging.name": "Excavación de Olfateador", "table.minecraft.pots.trial_chambers.corridor.name": "Cámara de Desafío - Jarrón del Corredor", "table.minecraft.shearing.bogged.name": "Atascado - Trasquilar", "table.minecraft.spawners.ominous.trial_chamber.consumables.name": "Cámara de Desafío - Consumibles Ominosos", "table.minecraft.spawners.ominous.trial_chamber.key.name": "Cámara de Desafío - Llave Ominosa", "table.minecraft.spawners.trial_chamber.consumables.name": "Cámara de Desafío - Consumibles", "table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name": "Cámara de Desafío - Botín Ominoso", "table.minecraft.spawners.trial_chamber.key.name": "Cámara de Desafío - Llave", "table.builtin.block_drops": "Bloque - %s" } ================================================ FILE: common/src/main/resources/assets/bookshelf/lang/ja_jp.json ================================================ { "__formatting": "", "format.bookshelf.right": "%s:%s", "format.bookshelf.center": "%s:%s", "format.bookshelf.left": "%s:%s", "format.bookshelf.spaced": "%s %s", "format.bookshelf.none": "%s%s", "format.bookshelf.unit_rate": "%s/%s", "__commands": "Command Text", "commands.bookshelf.hand.error.not_air": "アイテムは空または空気であってはいけません", "commands.bookshelf.hand.error.internal": "テキストの書式設定中にエラーが発生しました。詳細はログを確認してください", "commands.bookshelf.font.unsupported_block": "「%s」にフォントを適用できませんでした。この種類のブロックはサポートされていません", "commands.bookshelf.font.bad_sender": "フォント名変更コマンド", "commands.bookshelf.debug.no_info": "このコマンドにデバッグ情報はありません", "commands.bookshelf.debug.yes_info": "デバッグ情報がゲームのログに記録されました。クリックして出力をコピーします", "commands.bookshelf.debug.too_long": "デバッグ出力が長すぎるためチャットに表示できません。ゲームのログを確認してください", "commands.bookshelf.structure.error.no_structures": "構造物が見つかりませんでした", "commands.bookshelf.structure.found": "%s個の構造物を発見しました!", "__time_units": "Entries for various time units that may be displayed in game.", "units.bookshelf.tick": "ティック", "units.bookshelf.tick.plural": "ティック", "units.bookshelf.tick.abbreviated": "t", "units.bookshelf.nanosecond": "ナノ秒", "units.bookshelf.nanosecond.plural": "ナノ秒", "units.bookshelf.nanosecond.abbreviated": "ns", "units.bookshelf.millisecond": "ミリ秒", "units.bookshelf.millisecond.plural": "ミリ秒", "units.bookshelf.millisecond.abbreviated": "ms", "units.bookshelf.second": "秒", "units.bookshelf.second.plural": "秒", "units.bookshelf.second.abbreviated": "s", "units.bookshelf.minute": "分", "units.bookshelf.minute.plural": "分", "units.bookshelf.minute.abbreviated": "m", "units.bookshelf.hour": "時間", "units.bookshelf.hour.plural": "時間", "units.bookshelf.hour.abbreviated": "h", "units.bookshelf.day": "日", "units.bookshelf.day.plural": "日", "units.bookshelf.day.abbreviated": "d", "units.bookshelf.week": "週", "units.bookshelf.week.plural": "週", "units.bookshelf.week.abbreviated": "wk", "units.bookshelf.month": "月", "units.bookshelf.month.plural": "月", "units.bookshelf.month.abbreviated": "mo", "units.bookshelf.year": "年", "units.bookshelf.year.plural": "年", "units.bookshelf.year.abbreviated": "yr", "__months": "Names of the months", "month.bookshelf.january": "1月", "month.bookshelf.february": "2月", "month.bookshelf.march": "3月", "month.bookshelf.april": "4月", "month.bookshelf.may": "5月", "month.bookshelf.june": "6月", "month.bookshelf.july": "7月", "month.bookshelf.august": "8月", "month.bookshelf.september": "9月", "month.bookshelf.october": "10月", "month.bookshelf.november": "11月", "month.bookshelf.december": "12月", "__days": "Names of the days", "day.bookshelf.sunday": "日曜日", "day.bookshelf.monday": "月曜日", "day.bookshelf.tuesday": "火曜日", "day.bookshelf.wednesday": "水曜日", "day.bookshelf.thursday": "木曜日", "day.bookshelf.friday": "金曜日", "day.bookshelf.saturday": "土曜日", "__moon_phases": "The names of different moon phases that appear in game.", "moon.phase.full": "満月", "moon.phase.waxing.gibbous": "十三夜", "moon.phase.first.quarter": "上弦", "moon.phase.waxing.crescent": "三日月", "moon.phase.new": "新月", "moon.phase.waning.crescent": "二十五夜", "moon.phase.last.quarter": "下弦", "moon.phase.waning.gibbous": "十八夜", "__fonts": "Unofficial language entries for the vanilla text fonts.", "font.minecraft.default": "デフォルト", "font.minecraft.default.desc": "Minecraftの標準フォント", "font.minecraft.default.preview": "The quick brown fox jumps over the lazy dog.", "font.minecraft.alt": "標準銀河系アルファベット", "font.minecraft.alt.desc": "エンチャントや魔術に関連するルーン文字ベースのフォント", "font.minecraft.alt.preview": "Majik fox cub solved the waspy dragons quiz", "font.minecraft.illageralt": "Illager Alphabet", "font.minecraft.illageralt.desc": "邪悪な村人が使う謎の多いフォント", "font.minecraft.illageralt.preview": "Grumpy wizards make a toxic brew for the jovial queen.", "font.minecraft.uniform": "Uniform", "font.minecraft.uniform.desc": "様式化されていないプレーンなフォント", "font.minecraft.uniform.preview": "The quick brown fox jumps over the lazy dog.", "__Tags": "Tag Names for Recipe Viewers", "tag.item.bookshelf.creative_tab.minecraft.colored_blocks": "色付きブロックタブ", "tag.item.bookshelf.creative_tab.minecraft.food_and_drinks": "食べ物と飲み物タブ", "tag.item.bookshelf.creative_tab.minecraft.spawn_eggs": "スポーンエッグタブ", "tag.item.bookshelf.creative_tab.minecraft.redstone_blocks": "レッドストーン系ブロックタブ", "tag.item.bookshelf.creative_tab.minecraft.op_blocks": "管理者用アイテムタブ", "tag.item.bookshelf.creative_tab.minecraft.combat": "戦闘タブ", "tag.item.bookshelf.creative_tab.minecraft.building_blocks": "建築ブロックタブ", "tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities": "道具と実用品タブ", "tag.item.bookshelf.creative_tab.minecraft.natural_blocks": "天然ブロックタブ", "tag.item.bookshelf.creative_tab.minecraft.functional_blocks": "機能的ブロックタブ", "tag.item.bookshelf.creative_tab.minecraft.ingredients": "材料タブ", "tooltips.bookshelf.loot.unknown": "不明なドロップ", "tooltips.bookshelf.loot.unknown.desc": "現在、一部のドロップアイテムを表示できません", "tooltips.bookshelf.loot.empty": "空のドロップ", "tooltips.bookshelf.loot.empty.desc": "何もドロップしない可能性があります!", "tooltips.bookshelf.loot.dynamic": "動的ドロップ", "tooltips.bookshelf.loot.dynamic.desc": "特定の条件に依存したドロップアイテムがあります", "gui.jei.category.loot.name": "ルートテーブル", "table.minecraft.archaeology.desert_pyramid.name": "砂漠の寺院", "table.minecraft.archaeology.desert_well.name": "砂漠の井戸", "table.minecraft.archaeology.ocean_ruin_cold.name": "海底遺跡 - 冷たい海域", "table.minecraft.archaeology.ocean_ruin_warm.name": "海底遺跡 - 暖かい海域", "table.minecraft.archaeology.trail_ruins_common.name": "旅路の遺跡 - 普通", "table.minecraft.archaeology.trail_ruins_rare.name": "旅路の遺跡 - 稀少品", "table.minecraft.chests.abandoned_mineshaft.name": "廃坑", "table.minecraft.chests.ancient_city.name": "古代都市", "table.minecraft.chests.ancient_city_ice_box.name": "古代都市 - 氷室", "table.minecraft.chests.bastion_bridge.name": "砦の遺跡 - 橋", "table.minecraft.chests.bastion_hoglin_stable.name": "砦の遺跡 - ホグリンの小屋", "table.minecraft.chests.bastion_other.name": "砦の遺跡 - 一般", "table.minecraft.chests.bastion_treasure.name": "砦の遺跡 - 宝物部屋", "table.minecraft.chests.buried_treasure.name": "埋もれた宝", "table.minecraft.chests.desert_pyramid.name": "砂漠の寺院", "table.minecraft.chests.end_city_treasure.name": "エンドシティ - 宝物", "table.minecraft.chests.igloo_chest.name": "イグルー - チェスト", "table.minecraft.chests.jungle_temple.name": "ジャングルの寺院 - チェスト", "table.minecraft.chests.jungle_temple_dispenser.name": "ジャングルの寺院 - ディスペンサー", "table.minecraft.chests.nether_bridge.name": "ネザー要塞", "table.minecraft.chests.pillager_outpost.name": "ピリジャーの前哨基地", "table.minecraft.chests.ruined_portal.name": "荒廃したポータル", "table.minecraft.chests.shipwreck_map.name": "難破船 - 地図入り", "table.minecraft.chests.shipwreck_supply.name": "難破船 - 補給物資", "table.minecraft.chests.shipwreck_treasure.name": "難破船 - 宝物", "table.minecraft.chests.simple_dungeon.name": "ダンジョン", "table.minecraft.chests.spawn_bonus_chest.name": "ボーナスチェスト", "table.minecraft.chests.stronghold_corridor.name": "要塞 - 祭壇", "table.minecraft.chests.stronghold_crossing.name": "要塞 - 倉庫", "table.minecraft.chests.stronghold_library.name": "要塞 - 図書室", "table.minecraft.chests.trial_chambers.corridor.name": "試練の間 - 廊下", "table.minecraft.chests.trial_chambers.entrance.name": "試練の間 - 玄関", "table.minecraft.chests.trial_chambers.intersection.name": "試練の間 - 交差の部屋", "table.minecraft.chests.trial_chambers.intersection_barrel.name": "試練の間 - 交差の部屋の樽", "table.minecraft.chests.trial_chambers.reward.name": "試練の間 - 報酬", "table.minecraft.chests.trial_chambers.reward_common.name": "試練の間 - 通常の報酬", "table.minecraft.chests.trial_chambers.reward_ominous.name": "試練の間 - 不吉な報酬", "table.minecraft.chests.trial_chambers.reward_ominous_common.name": "試練の間 - 不吉な報酬 - 普通", "table.minecraft.chests.trial_chambers.reward_ominous_rare.name": "試練の間 - 不吉な報酬 - 希少品", "table.minecraft.chests.trial_chambers.reward_ominous_unique.name": "試練の間 - 不吉な報酬 - ユニーク", "table.minecraft.chests.trial_chambers.reward_rare.name": "試練の間 - 希少な報酬", "table.minecraft.chests.trial_chambers.reward_unique.name": "試練の間 - ユニークな報酬", "table.minecraft.chests.trial_chambers.supply.name": "試練の間 - 補給物資", "table.minecraft.chests.underwater_ruin_big.name": "海底遺跡 - 大", "table.minecraft.chests.underwater_ruin_small.name": "海底遺跡 - 小", "table.minecraft.chests.village.village_armorer.name": "村 - 防具鍛冶の家", "table.minecraft.chests.village.village_butcher.name": "村 - 肉屋の店", "table.minecraft.chests.village.village_cartographer.name": "村 - 製図家の家", "table.minecraft.chests.village.village_desert_house.name": "村 - 砂漠の村の家", "table.minecraft.chests.village.village_fisher.name": "村 - 釣り小屋", "table.minecraft.chests.village.village_fletcher.name": "村 - 矢師の家", "table.minecraft.chests.village.village_mason.name": "村 - 石工の家", "table.minecraft.chests.village.village_plains_house.name": "村 - 平原の村の家", "table.minecraft.chests.village.village_savanna_house.name": "村 - サバンナの村の家", "table.minecraft.chests.village.village_shepherd.name": "村 - 羊飼いの家", "table.minecraft.chests.village.village_snowy_house.name": "村 - 雪原の村の家", "table.minecraft.chests.village.village_taiga_house.name": "村 - タイガの村の家", "table.minecraft.chests.village.village_tannery.name": "村 - 革加工場", "table.minecraft.chests.village.village_temple.name": "村 - 礼拝所", "table.minecraft.chests.village.village_toolsmith.name": "村 - 道具鍛冶場", "table.minecraft.chests.village.village_weaponsmith.name": "村 - 武器鍛冶場", "table.minecraft.chests.woodland_mansion.name": "森の洋館", "table.minecraft.dispensers.trial_chambers.chamber.name": "試練の間 - 試練室", "table.minecraft.dispensers.trial_chambers.corridor.name": "試練の間 - 廊下", "table.minecraft.dispensers.trial_chambers.water.name": "試練の間 - 水", "table.minecraft.empty.name": "空", "table.minecraft.equipment.trial_chamber.name": "試練の間", "table.minecraft.equipment.trial_chamber_melee.name": "試練の間 - 近接攻撃型", "table.minecraft.equipment.trial_chamber_ranged.name": "試練の間 - 遠隔攻撃型", "table.minecraft.gameplay.cat_morning_gift.name": "ネコ - 贈り物", "table.minecraft.gameplay.fishing.fish.name": "釣り - 魚", "table.minecraft.gameplay.fishing.junk.name": "釣り - ゴミ", "table.minecraft.gameplay.fishing.name": "釣り", "table.minecraft.gameplay.fishing.treasure.name": "釣り - 宝", "table.minecraft.gameplay.hero_of_the_village.armorer_gift.name": "村の英雄 - 防具鍛冶", "table.minecraft.gameplay.hero_of_the_village.butcher_gift.name": "村の英雄 - 肉屋", "table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name": "村の英雄 - 製図家", "table.minecraft.gameplay.hero_of_the_village.cleric_gift.name": "村の英雄 - 聖職者", "table.minecraft.gameplay.hero_of_the_village.farmer_gift.name": "村の英雄 - 農民", "table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name": "村の英雄 - 釣り人", "table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name": "村の英雄 - 矢師", "table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name": "村の英雄 - 革細工師", "table.minecraft.gameplay.hero_of_the_village.librarian_gift.name": "村の英雄 - 司書", "table.minecraft.gameplay.hero_of_the_village.mason_gift.name": "村の英雄 - 石工", "table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name": "村の英雄 - 羊飼い", "table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name": "村の英雄 - 道具鍛冶", "table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name": "村の英雄 - 武器鍛冶", "table.minecraft.gameplay.panda_sneeze.name": "パンダのくしゃみ", "table.minecraft.gameplay.piglin_bartering.name": "ピグリンとの物々交換", "table.minecraft.gameplay.sniffer_digging.name": "スニッファーが掘り出す", "table.minecraft.pots.trial_chambers.corridor.name": "試練の間 - 廊下の飾り壺", "table.minecraft.shearing.bogged.name": "ボグド - 刈り取り", "table.minecraft.spawners.ominous.trial_chamber.consumables.name": "試練の間 - 不吉な消耗品", "table.minecraft.spawners.ominous.trial_chamber.key.name": "試練の間 - 不吉な鍵", "table.minecraft.spawners.trial_chamber.consumables.name": "試練の間 - 消耗品", "table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name": "試練の間 - 不吉なアイテムスポナー", "table.minecraft.spawners.trial_chamber.key.name": "試練の間 - 鍵", "table.builtin.block_drops": "ブロック - %s" } ================================================ FILE: common/src/main/resources/assets/bookshelf/lang/pt_br.json ================================================ { "__formatting": "", "format.bookshelf.right": "%s: %s", "format.bookshelf.center": "%s : %s", "format.bookshelf.left": "%s :%s", "format.bookshelf.spaced": "%s %s", "format.bookshelf.none": "%s%s", "format.bookshelf.unit_rate": "%s/%s", "__commands": "Command Text", "commands.bookshelf.hand.error.not_air": "O item não deve estar vazio ou ser ar!", "commands.bookshelf.hand.error.internal": "Erro encontrado ao formatar o texto. Verifique os logs para mais informações.", "commands.bookshelf.font.unsupported_block": "Não foi possível aplicar a fonte a '%s'. Este tipo de bloco não é suportado.", "commands.bookshelf.font.bad_sender": "O comando de renomear fonte", "commands.bookshelf.debug.no_info": "Nenhuma informação de depuração disponível para este comando.", "commands.bookshelf.debug.yes_info": "As informações de depuração foram registradas no seu log do jogo. Clique para copiar a saída.", "commands.bookshelf.debug.too_long": "A saída de depuração é muito grande para o chat. Por favor, verifique o seu log do jogo.", "commands.bookshelf.structure.error.no_structures": "Nenhuma estrutura pôde ser encontrada.", "commands.bookshelf.structure.found": "Encontrada(s) %s estrutura(s)!", "__time_units": "Entries for various time units that may be displayed in game.", "units.bookshelf.tick": "Tick", "units.bookshelf.tick.plural": "Ticks", "units.bookshelf.tick.abbreviated": "t", "units.bookshelf.nanosecond": "Nanossegundo", "units.bookshelf.nanosecond.plural": "Nanossegundos", "units.bookshelf.nanosecond.abbreviated": "ns", "units.bookshelf.millisecond": "Milissegundo", "units.bookshelf.millisecond.plural": "Milissegundos", "units.bookshelf.millisecond.abbreviated": "ms", "units.bookshelf.second": "Segundo", "units.bookshelf.second.plural": "Segundos", "units.bookshelf.second.abbreviated": "s", "units.bookshelf.minute": "Minuto", "units.bookshelf.minute.plural": "Minutos", "units.bookshelf.minute.abbreviated": "m", "units.bookshelf.hour": "Hora", "units.bookshelf.hour.plural": "Horas", "units.bookshelf.hour.abbreviated": "h", "units.bookshelf.day": "Dia", "units.bookshelf.day.plural": "Dias", "units.bookshelf.day.abbreviated": "d", "units.bookshelf.week": "Semana", "units.bookshelf.week.plural": "Semanas", "units.bookshelf.week.abbreviated": "sem", "units.bookshelf.month": "Mês", "units.bookshelf.month.plural": "Meses", "units.bookshelf.month.abbreviated": "mês", "units.bookshelf.year": "Ano", "units.bookshelf.year.plural": "Anos", "units.bookshelf.year.abbreviated": "a", "__months": "Names of the months", "month.bookshelf.january": "Janeiro", "month.bookshelf.february": "Fevereiro", "month.bookshelf.march": "Março", "month.bookshelf.april": "Abril", "month.bookshelf.may": "Maio", "month.bookshelf.june": "Junho", "month.bookshelf.july": "Julho", "month.bookshelf.august": "Agosto", "month.bookshelf.september": "Setembro", "month.bookshelf.october": "Outubro", "month.bookshelf.november": "Novembro", "month.bookshelf.december": "Dezembro", "__days": "Names of the days", "day.bookshelf.sunday": "Domingo", "day.bookshelf.monday": "Segunda-feira", "day.bookshelf.tuesday": "Terça-feira", "day.bookshelf.wednesday": "Quarta-feira", "day.bookshelf.thursday": "Quinta-feira", "day.bookshelf.friday": "Sexta-feira", "day.bookshelf.saturday": "Sábado", "__moon_phases": "The names of different moon phases that appear in game.", "moon.phase.full": "Lua Cheia", "moon.phase.waxing.gibbous": "Gibosa Crescente", "moon.phase.first.quarter": "Quarto Crescente", "moon.phase.waxing.crescent": "Lua Crescente", "moon.phase.new": "Lua Nova", "moon.phase.waning.crescent": "Lua Minguante", "moon.phase.last.quarter": "Quarto Minguante", "moon.phase.waning.gibbous": "Gibosa Minguante", "__fonts": "Unofficial language entries for the vanilla text fonts.", "font.minecraft.default": "Padrão", "font.minecraft.default.desc": "A fonte padrão do Minecraft.", "font.minecraft.default.preview": "A rápida raposa marrom salta sobre o cão preguiçoso.", "font.minecraft.alt": "Alfabeto Galáctico Padrão", "font.minecraft.alt.desc": "Uma fonte baseada em runas associada a encantamentos e magia.", "font.minecraft.alt.preview": "O filhote de raposa mágico resolveu o enigma dos dragões vespertinos", "font.minecraft.illageralt": "Alfabeto Illager", "font.minecraft.illageralt.desc": "Uma fonte misteriosa usada pelos illagers.", "font.minecraft.illageralt.preview": "Magos mal-humorados fazem uma poção tóxica para a rainha jovial.", "font.minecraft.uniform": "Uniforme", "font.minecraft.uniform.desc": "Uma fonte simples que não é estilizada.", "font.minecraft.uniform.preview": "A rápida raposa marrom salta sobre o cão preguiçoso.", "__Tags": "Tag Names for Recipe Viewers", "tag.item.bookshelf.creative_tab.minecraft.colored_blocks": "Aba de Blocos Coloridos", "tag.item.bookshelf.creative_tab.minecraft.food_and_drinks": "Aba de Comidas e Bebidas", "tag.item.bookshelf.creative_tab.minecraft.spawn_eggs": "Aba de Ovos de Invocação", "tag.item.bookshelf.creative_tab.minecraft.redstone_blocks": "Aba de Redstone", "tag.item.bookshelf.creative_tab.minecraft.op_blocks": "Aba de OP/Admin", "tag.item.bookshelf.creative_tab.minecraft.combat": "Aba de Combate", "tag.item.bookshelf.creative_tab.minecraft.building_blocks": "Aba de Blocos de Construção", "tag.item.bookshelf.creative_tab.minecraft.tools_and_utilities": "Aba de Ferramentas e Utilidades", "tag.item.bookshelf.creative_tab.minecraft.natural_blocks": "Aba de Blocos Naturais", "tag.item.bookshelf.creative_tab.minecraft.functional_blocks": "Aba de Blocos Funcionais", "tag.item.bookshelf.creative_tab.minecraft.ingredients": "Aba de Ingredientes", "tooltips.bookshelf.loot.unknown": "Drop Desconhecido", "tooltips.bookshelf.loot.unknown.desc": "Alguns drops não podem ser exibidos no momento.", "tooltips.bookshelf.loot.empty": "Drop Vazio", "tooltips.bookshelf.loot.empty.desc": "É possível que nada seja dropado!", "tooltips.bookshelf.loot.dynamic": "Drop Dinâmico", "tooltips.bookshelf.loot.dynamic.desc": "Alguns drops dependem do contexto.", "gui.jei.category.loot.name": "Tabela de Loot", "table.minecraft.archaeology.desert_pyramid.name": "Pirâmide do Deserto", "table.minecraft.archaeology.desert_well.name": "Poço do Deserto", "table.minecraft.archaeology.ocean_ruin_cold.name": "Ruína do Oceano - Fria", "table.minecraft.archaeology.ocean_ruin_warm.name": "Ruína do Oceano - Quente", "table.minecraft.archaeology.trail_ruins_common.name": "Ruínas de Trilha - Comum", "table.minecraft.archaeology.trail_ruins_rare.name": "Ruínas de Trilha - Raro", "table.minecraft.chests.abandoned_mineshaft.name": "Mina Abandonada", "table.minecraft.chests.ancient_city.name": "Cidade Antiga", "table.minecraft.chests.ancient_city_ice_box.name": "Caixa de Gelo da Cidade Antiga", "table.minecraft.chests.bastion_bridge.name": "Bastião - Ponte", "table.minecraft.chests.bastion_hoglin_stable.name": "Bastião - Estábulo de Hoglins", "table.minecraft.chests.bastion_other.name": "Bastião - Outro", "table.minecraft.chests.bastion_treasure.name": "Bastião - Tesouro", "table.minecraft.chests.buried_treasure.name": "Tesouro Enterrado", "table.minecraft.chests.desert_pyramid.name": "Pirâmide do Deserto", "table.minecraft.chests.end_city_treasure.name": "Tesouro da Cidade do Fim", "table.minecraft.chests.igloo_chest.name": "Baú do Iglu", "table.minecraft.chests.jungle_temple.name": "Templo da Selva - Baú", "table.minecraft.chests.jungle_temple_dispenser.name": "Templo da Selva - Ejetor", "table.minecraft.chests.nether_bridge.name": "Ponte do Nether", "table.minecraft.chests.pillager_outpost.name": "Posto Avançado de Saqueadores", "table.minecraft.chests.ruined_portal.name": "Portal em Ruínas", "table.minecraft.chests.shipwreck_map.name": "Mapa de Naufrágio", "table.minecraft.chests.shipwreck_supply.name": "Naufrágio - Suprimentos", "table.minecraft.chests.shipwreck_treasure.name": "Naufrágio - Tesouro", "table.minecraft.chests.simple_dungeon.name": "Masmorra", "table.minecraft.chests.spawn_bonus_chest.name": "Baú de Bônus Inicial", "table.minecraft.chests.stronghold_corridor.name": "Fortaleza - Corredor", "table.minecraft.chests.stronghold_crossing.name": "Fortaleza - Cruzamento", "table.minecraft.chests.stronghold_library.name": "Fortaleza - Biblioteca", "table.minecraft.chests.trial_chambers.corridor.name": "Câmara de Desafios - Corredor", "table.minecraft.chests.trial_chambers.entrance.name": "Câmara de Desafios - Entrada", "table.minecraft.chests.trial_chambers.intersection.name": "Câmara de Desafios - Interseção", "table.minecraft.chests.trial_chambers.intersection_barrel.name": "Câmara de Desafios - Barril da Interseção", "table.minecraft.chests.trial_chambers.reward.name": "Câmara de Desafios - Recompensa", "table.minecraft.chests.trial_chambers.reward_common.name": "Câmara de Desafios - Recompensa Comum", "table.minecraft.chests.trial_chambers.reward_ominous.name": "Câmara de Desafios - Recompensa Sinistra", "table.minecraft.chests.trial_chambers.reward_ominous_common.name": "Câmara de Desafios - Recompensa Sinistra - Comum", "table.minecraft.chests.trial_chambers.reward_ominous_rare.name": "Câmara de Desafios - Recompensa Sinistra - Rara", "table.minecraft.chests.trial_chambers.reward_ominous_unique.name": "Câmara de Desafios - Recompensa Sinistra - Única", "table.minecraft.chests.trial_chambers.reward_rare.name": "Câmara de Desafios - Recompensa Rara", "table.minecraft.chests.trial_chambers.reward_unique.name": "Câmara de Desafios - Recompensa Única", "table.minecraft.chests.trial_chambers.supply.name": "Câmara de Desafios - Suprimentos", "table.minecraft.chests.underwater_ruin_big.name": "Ruína Subaquática - Grande", "table.minecraft.chests.underwater_ruin_small.name": "Ruína Subaquática - Pequena", "table.minecraft.chests.village.village_armorer.name": "Vila - Armeiro", "table.minecraft.chests.village.village_butcher.name": "Vila - Açougueiro", "table.minecraft.chests.village.village_cartographer.name": "Vila - Cartógrafo", "table.minecraft.chests.village.village_desert_house.name": "Vila - Casa do Deserto", "table.minecraft.chests.village.village_fisher.name": "Vila - Pescador", "table.minecraft.chests.village.village_fletcher.name": "Vila - Flecheiro", "table.minecraft.chests.village.village_mason.name": "Vila - Pedreiro", "table.minecraft.chests.village.village_plains_house.name": "Vila - Casa da Planície", "table.minecraft.chests.village.village_savanna_house.name": "Vila - Casa da Savana", "table.minecraft.chests.village.village_shepherd.name": "Vila - Pastor", "table.minecraft.chests.village.village_snowy_house.name": "Vila - Casa de Neve", "table.minecraft.chests.village.village_taiga_house.name": "Vila - Casa da Taiga", "table.minecraft.chests.village.village_tannery.name": "Vila - Curtume", "table.minecraft.chests.village.village_temple.name": "Vila - Templo", "table.minecraft.chests.village.village_toolsmith.name": "Vila - Ferreiro (Ferramentas)", "table.minecraft.chests.village.village_weaponsmith.name": "Vila - Ferreiro (Armas)", "table.minecraft.chests.woodland_mansion.name": "Mansão da Floresta", "table.minecraft.dispensers.trial_chambers.chamber.name": "Câmara de Desafios - Câmara", "table.minecraft.dispensers.trial_chambers.corridor.name": "Câmara de Desafios - Corredor", "table.minecraft.dispensers.trial_chambers.water.name": "Câmara de Desafios - Água", "table.minecraft.empty.name": "Vazio", "table.minecraft.equipment.trial_chamber.name": "Câmara de Desafios", "table.minecraft.equipment.trial_chamber_melee.name": "Câmara de Desafios - Corpo a Corpo", "table.minecraft.equipment.trial_chamber_ranged.name": "Câmara de Desafios - À Distância", "table.minecraft.gameplay.cat_morning_gift.name": "Gato - Presente Matinal", "table.minecraft.gameplay.fishing.fish.name": "Pesca - Peixe", "table.minecraft.gameplay.fishing.junk.name": "Pesca - Lixo", "table.minecraft.gameplay.fishing.name": "Pesca", "table.minecraft.gameplay.fishing.treasure.name": "Pesca - Tesouro", "table.minecraft.gameplay.hero_of_the_village.armorer_gift.name": "Herói da Vila - Armeiro", "table.minecraft.gameplay.hero_of_the_village.butcher_gift.name": "Herói da Vila - Açougueiro", "table.minecraft.gameplay.hero_of_the_village.cartographer_gift.name": "Herói da Vila - Cartógrafo", "table.minecraft.gameplay.hero_of_the_village.cleric_gift.name": "Herói da Vila - Clérigo", "table.minecraft.gameplay.hero_of_the_village.farmer_gift.name": "Herói da Vila - Fazendeiro", "table.minecraft.gameplay.hero_of_the_village.fisherman_gift.name": "Herói da Vila - Pescador", "table.minecraft.gameplay.hero_of_the_village.fletcher_gift.name": "Herói da Vila - Flecheiro", "table.minecraft.gameplay.hero_of_the_village.leatherworker_gift.name": "Herói da Vila - Coureiro", "table.minecraft.gameplay.hero_of_the_village.librarian_gift.name": "Herói da Vila - Bibliotecário", "table.minecraft.gameplay.hero_of_the_village.mason_gift.name": "Herói da Vila - Pedreiro", "table.minecraft.gameplay.hero_of_the_village.shepherd_gift.name": "Herói da Vila - Pastor", "table.minecraft.gameplay.hero_of_the_village.toolsmith_gift.name": "Herói da Vila - Ferreiro (Ferramentas)", "table.minecraft.gameplay.hero_of_the_village.weaponsmith_gift.name": "Herói da Vila - Ferreiro (Armas)", "table.minecraft.gameplay.panda_sneeze.name": "Espirro de Panda", "table.minecraft.gameplay.piglin_bartering.name": "Troca com Piglins", "table.minecraft.gameplay.sniffer_digging.name": "Escavação do Farejador", "table.minecraft.pots.trial_chambers.corridor.name": "Câmara de Desafios - Vaso do Corredor", "table.minecraft.shearing.bogged.name": "Atolado - Tosquia", "table.minecraft.spawners.ominous.trial_chamber.consumables.name": "Câmara de Desafios - Consumíveis Sinistros", "table.minecraft.spawners.ominous.trial_chamber.key.name": "Câmara de Desafios - Chave Sinistra", "table.minecraft.spawners.trial_chamber.consumables.name": "Câmara de Desafios - Consumíveis", "table.minecraft.spawners.trial_chamber.items_to_drop_when_ominous.name": "Câmara de Desafios - Drops Sinistros", "table.minecraft.spawners.trial_chamber.key.name": "Câmara de Desafios - Chave", "table.builtin.block_drops": "Bloco - %s" } ================================================ FILE: common/src/main/resources/assets/bookshelf/lang/zh_cn.json ================================================ { "__formatting": "", "format.bookshelf.right": "%s: %s", "format.bookshelf.center": "%s : %s", "format.bookshelf.left": "%s :%s", "format.bookshelf.spaced": "%s %s", "format.bookshelf.none": "%s%s", "format.bookshelf.unit_rate": "%s/%s", "__commands": "命令文本", "commands.bookshelf.hand.error.not_air": "物品不能为空或空气!", "commands.bookshelf.font.unsupported_block": "无法将字体应用到 '%s'。不支持这种方块", "commands.bookshelf.font.bad_sender": "字体重命名命令", "__time_units": "游戏中可能显示的各种时间单位的条目", "units.bookshelf.tick": "刻", "units.bookshelf.tick.plural": "刻", "units.bookshelf.tick.abbreviated": "t", "units.bookshelf.nanosecond": "纳秒", "units.bookshelf.nanosecond.plural": "纳秒", "units.bookshelf.nanosecond.abbreviated": "ns", "units.bookshelf.millisecond": "毫秒", "units.bookshelf.millisecond.plural": "毫秒", "units.bookshelf.millisecond.abbreviated": "ms", "units.bookshelf.second": "秒", "units.bookshelf.second.plural": "秒", "units.bookshelf.second.abbreviated": "s", "units.bookshelf.minute": "分钟", "units.bookshelf.minute.plural": "分钟", "units.bookshelf.minute.abbreviated": "m", "units.bookshelf.hour": "小时", "units.bookshelf.hour.plural": "小时", "units.bookshelf.hour.abbreviated": "h", "units.bookshelf.day": "日", "units.bookshelf.day.plural": "日", "units.bookshelf.day.abbreviated": "d", "units.bookshelf.week": "周", "units.bookshelf.week.plural": "周", "units.bookshelf.week.abbreviated": "wk", "units.bookshelf.month": "月", "units.bookshelf.month.plural": "月", "units.bookshelf.month.abbreviated": "mo", "units.bookshelf.year": "年", "units.bookshelf.year.plural": "年", "units.bookshelf.year.abbreviated": "yr", "__months": "月份名称", "month.bookshelf.january": "一月", "month.bookshelf.february": "二月", "month.bookshelf.march": "三月", "month.bookshelf.april": "四月", "month.bookshelf.may": "五月", "month.bookshelf.june": "六月", "month.bookshelf.july": "七月", "month.bookshelf.august": "八月", "month.bookshelf.september": "九月", "month.bookshelf.october": "十月", "month.bookshelf.november": "十一月", "month.bookshelf.december": "十二月", "__days": "日期名称", "day.bookshelf.sunday": "星期日", "day.bookshelf.monday": "星期一", "day.bookshelf.tuesday": "星期二", "day.bookshelf.wednesday": "星期三", "day.bookshelf.thursday": "星期四", "day.bookshelf.friday": "星期五", "day.bookshelf.saturday": "星期六", "__moon_phases": "游戏中出现的不同月相的名称", "moon.phase.full": "满月", "moon.phase.waxing.gibbous": "盈凸月", "moon.phase.first.quarter": "上弦月", "moon.phase.waxing.crescent": "蛾眉月", "moon.phase.new": "新月", "moon.phase.waning.crescent": "残月", "moon.phase.last.quarter": "下弦月", "moon.phase.waning.gibbous": "亏凸月", "__fonts": "原版字体的非官方语言条目", "font.minecraft.default": "默认", "font.minecraft.default.desc": "Minecraft 中的标准字体", "font.minecraft.default.preview": "The quick brown fox jumps over the lazy dog.", "font.minecraft.alt": "标准星系字母", "font.minecraft.alt.desc": "一种基于符文的字体,与魔法和魔法有关", "font.minecraft.alt.preview": "Majik fox cub solved the waspy dragons quiz", "font.minecraft.illageralt": "灾厄村民", "font.minecraft.illageralt.desc": "灾厄村民使用的神秘字体", "font.minecraft.illageralt.preview": "Grumpy wizards make a toxic brew for the jovial queen.", "font.minecraft.uniform": "统一字体", "font.minecraft.uniform.desc": "一种没有风格的普通字体", "font.minecraft.uniform.preview": "The quick brown fox jumps over the lazy dog." } ================================================ FILE: common/src/main/resources/bookshelf.mixins.json ================================================ { "required": true, "minVersion": "0.8", "package": "net.darkhax.bookshelf.common.mixin", "refmap": "${mod_id}.refmap.json", "compatibilityLevel": "JAVA_18", "mixins": [ "access.block.AccessorBannerBlockEntity", "access.block.AccessorBaseContainerBlockEntity", "access.block.AccessorCropBlock", "access.entity.AccessorEntity", "access.level.AccessorRecipeManager", "access.loot.AccessorCompositeEntryBase", "access.loot.AccessorDynamicLoot", "access.loot.AccessorLootItem", "access.loot.AccessorLootPool", "access.loot.AccessorLootPoolSingletonContainer", "access.loot.AccessorLootTable", "access.loot.AccessorNestedLootTable", "access.loot.AccessorTagEntry", "access.particles.AccessSimpleParticleType", "patch.advancement.MixinPlayerAdvancements", "patch.block.MixinDecoratedPotPatterns", "patch.entity.MixinLightningBolt", "patch.entity.MixinLivingEntity", "patch.item.MixinCreativeModeTab", "patch.level.MixinRecipeManager", "patch.level.MixinWalkNodeEvaluator", "patch.loot.MixinLootDataType", "patch.loot.MixinLootItemKilledByPlayerCondition", "patch.loot.MixinLootPool", "patch.packs.MixinSimpleJsonResourceReloadListener", "patch.potions.MixinPotionBrewing", "patch.server.MixinReloadableServerResources" ], "client": [ "access.block.AccessorBlockEntityRenderers", "access.client.AccessorFontManager", "access.client.AccessorItemBlockRenderTypes", "access.client.AccessorMinecraft", "access.client.gui.AccessorAbstractWidget", "patch.client.MixinClientPacketListener", "patch.locale.MixinClientLanguage" ], "server": [], "injectors": { "defaultRequire": 1 } } ================================================ FILE: common/src/main/resources/data/bookshelf/damage_type/fake_player.json ================================================ { "exhaustion": 0.1, "message_id": "player", "scaling": "when_caused_by_living_non_player" } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/damage_type/fake_player.json ================================================ { "comments": [ "Damage types in this tag are considered fake player damage. Fake player ", "damage acts like regular player damage but does not require an associated", "player entity. This allows something like a block or mob to deal damage ", "that causes mobs to drop exp and player only loot when killed. " ], "values": [ "bookshelf:fake_player" ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/building_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/colored_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/combat.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/food_and_drinks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/functional_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/ingredients.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/natural_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/op_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/redstone_blocks.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/spawn_eggs.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/data/bookshelf/tags/item/creative_tab/minecraft/tools_and_utilities.json ================================================ { "values": [ ] } ================================================ FILE: common/src/main/resources/pack.mcmeta ================================================ { "pack": { "description": "${mod_name}", "pack_format": 8 } } ================================================ FILE: fabric/build.gradle ================================================ plugins { id 'multiloader-loader' id 'fabric-loom' id 'net.darkhax.curseforgegradle' id 'com.modrinth.minotaur' } if (project.hasProperty('modmenu_version')) { repositories { RepositoryHandler handler -> { limitedMaven(handler, 'https://maven.terraformersmc.com/', 'com.terraformersmc') }} dependencies { modRuntimeOnly("com.terraformersmc:modmenu:${project.findProperty('modmenu_version')}") } } dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" mappings loom.layered { officialMojangMappings() parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") } modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" if (project.hasProperty('jei_version')) { modCompileOnlyApi("mezz.jei:jei-${minecraft_version}-common-api:${jei_version}") modCompileOnlyApi("mezz.jei:jei-${minecraft_version}-fabric-api:${jei_version}") modRuntimeOnly("mezz.jei:jei-${minecraft_version}-fabric:${jei_version}") } } loom { mixin { defaultRefmapName.set("${mod_id}.refmap.json") } runs { client { client() setConfigName('Fabric Client') ideConfigGenerated(true) runDir('runs/client') } server { server() setConfigName('Fabric Server') ideConfigGenerated(true) runDir('runs/server') } } } // CurseForge task publishCurseForge(type: net.darkhax.curseforgegradle.TaskPublishCurseForge) { apiToken = rootProject.findProperty('curse_auth') var mainFile = upload(curse_project, tasks.remapJar) mainFile.changelogType = 'markdown' mainFile.changelog = rootProject.findProperty('mod_changelog') mainFile.addJavaVersion('Java 21') mainFile.addModLoader('Fabric') mainFile.addModLoader('Quilt') mainFile.releaseType = 'release' if (rootProject.hasProperty('mod_client_only') && rootProject.findProperty('mod_client_only') == 'true') { mainFile.addGameVersion('Client') } else { mainFile.addGameVersion('Server', 'Client') } // Append Patreon Supporters var patreonInfo = rootProject.findProperty('patreon') if (patreonInfo) { mainFile.changelog += "\n\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\n\n${patreonInfo.pledgeLog}" } } // Modrinth modrinth { var patreonInfo = rootProject.findProperty('patreon') var changelogText = rootProject.findProperty('mod_changelog') if (patreonInfo) { changelogText += "\n\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\n\n${patreonInfo.pledgeLog}" } token.set(rootProject.findProperty('modrinth_auth')) projectId.set(modrinth_project) changelog = changelogText versionName.set("${mod_name}-fabric-${minecraft_version}-$version") versionType.set("release") loaders = ["fabric", "quilt"] gameVersions = ["${minecraft_version}"] uploadFile.set(tasks.remapJar) dependencies { required.project("fabric-api") } } void limitedMaven(RepositoryHandler handler, String url, String... groups) { handler.exclusiveContent { it.forRepositories(handler.maven { setUrl(url) }) it.filter { f -> for (def group : groups) { f.includeGroup(group) } } } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/FabricMod.java ================================================ package net.darkhax.bookshelf.fabric.impl; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.BookshelfMod; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.fabric.impl.util.FabricRegistryHelper; import net.fabricmc.api.ModInitializer; import net.minecraft.DetectedVersion; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.CompletableFuture; public class FabricMod implements ModInitializer { @Override public void onInitialize() { BookshelfMod.getInstance().init(); Services.CONTENT.get().forEach(FabricRegistryHelper::new); CompletableFuture.runAsync(FabricMod::checkForUpdates); } private static void checkForUpdates() { try { final HttpURLConnection connection = (HttpURLConnection) new URL("https://updates.blamejared.com/get?n=" + Constants.MOD_ID + "&gv=" + DetectedVersion.BUILT_IN.getName() + "&ml=fabric").openConnection(); connection.setRequestMethod("HEAD"); int responseCode = connection.getResponseCode(); if (responseCode != 200) { Constants.LOG.warn("Version checker is not available."); } } catch (Exception e) { // TODO } } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/FabricModClient.java ================================================ package net.darkhax.bookshelf.fabric.impl; import net.fabricmc.api.ClientModInitializer; public class FabricModClient implements ClientModInitializer { @Override public void onInitializeClient() { } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/data/FabricIngredient.java ================================================ package net.darkhax.bookshelf.fabric.impl.data; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import java.util.List; import java.util.function.Supplier; public class FabricIngredient> implements CustomIngredient { private final T logic; private final Supplier> type; public FabricIngredient(T logic, Supplier> type) { this.logic = logic; this.type = type; } @Override public boolean test(ItemStack stack) { return this.logic.test(stack); } @Override public List getMatchingStacks() { return this.logic.getAllMatchingStacks(); } @Override public boolean requiresTesting() { return this.logic.requiresTesting(); } @Override public CustomIngredientSerializer getSerializer() { return this.type.get(); } public static > CustomIngredientSerializer> make(ResourceLocation id, MapCodec codec, StreamCodec stream) { final Supplier> typeLookup = () -> CustomIngredientSerializer.get(id); final MapCodec> ingredientCodec = codec.xmap(l -> new FabricIngredient<>(l, typeLookup), i -> i.logic); final StreamCodec> ingredientStream = stream.map(l -> new FabricIngredient<>(l, typeLookup), i -> i.logic); return new CustomIngredientSerializer<>() { @Override public ResourceLocation getIdentifier() { return id; } @Override public MapCodec> getCodec(boolean allowEmpty) { return ingredientCodec; } @Override public StreamCodec> getPacketCodec() { return ingredientStream; } }; } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/network/FabricNetworkHandler.java ================================================ package net.darkhax.bookshelf.fabric.impl.network; import net.darkhax.bookshelf.common.api.network.INetworkHandler; import net.darkhax.bookshelf.common.api.network.IPacket; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.client.Minecraft; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import java.util.HashMap; import java.util.Map; public class FabricNetworkHandler implements INetworkHandler { private static final Map> PACKETS = new HashMap<>(); @Override public void register(IPacket packet) { PayloadTypeRegistry.playC2S().register(packet.type(), packet.streamCodec()); PayloadTypeRegistry.playS2C().register(packet.type(), packet.streamCodec()); if (Services.PLATFORM.isPhysicalClient() && packet.destination().handledByClient()) { ClientPlayNetworking.registerGlobalReceiver(packet.type(), (payload, context) -> { context.client().execute(() -> { packet.handle(null, false, payload); }); }); } if (packet.destination().handledByServer()) { ServerPlayNetworking.registerGlobalReceiver(packet.type(), (payload, context) -> { context.server().execute(() -> { packet.handle(context.player(), true, payload); }); }); } PACKETS.put(packet.type().id(), packet); } @Override public void sendToServer(T payload) { final ResourceLocation id = payload.type().id(); if (!PACKETS.containsKey(id)) { Constants.LOG.error("Attempted to send unregistered packet {} to the server.", id); throw new IllegalStateException("Attempted to send unregistered packet " + id + " to the server."); } if (Minecraft.getInstance().player == null) { Constants.LOG.error("Attempted to send packet {} to the server before a player instance is available.", id); throw new IllegalStateException("Attempted to send packet " + id + " to the server before a player instance is available."); } ClientPlayNetworking.send(payload); } @Override public void sendToPlayer(ServerPlayer recipient, T payload) { final ResourceLocation id = payload.type().id(); if (!PACKETS.containsKey(id)) { Constants.LOG.error("Attempted to send unregistered packet {} to player {}.", id, recipient); throw new IllegalStateException("Attempted to send unregistered packet " + id + " to player " + recipient); } ServerPlayNetworking.send(recipient, payload); } @Override public boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId) { return ServerPlayNetworking.canSend(recipient, payloadId); } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricGameplayHelper.java ================================================ package net.darkhax.bookshelf.fabric.impl.util; import net.darkhax.bookshelf.common.api.util.IGameplayHelper; import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import java.util.function.BiFunction; public class FabricGameplayHelper implements IGameplayHelper { @Override public ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) { final int initialCount = stack.getCount(); final ItemStack result = IGameplayHelper.super.inventoryInsert(level, pos, side, stack); if (result.isEmpty() || result.getCount() != initialCount) { return result; } final Storage storage = ItemStorage.SIDED.find(level, pos, side); if (storage != null && storage.supportsInsertion()) { try (Transaction tx = Transaction.openOuter()) { final long count = storage.insert(ItemVariant.of(stack), stack.getCount(), tx); tx.commit(); if (count >= stack.getCount()) { return ItemStack.EMPTY; } else { final ItemStack txResult = stack.copy(); txResult.shrink((int) count); return txResult; } } } return stack; } @Override public BlockEntityType.Builder blockEntityBuilder(BiFunction factory, Block... validBlocks) { BlockEntityType.BlockEntitySupplier supplier = factory::apply; return BlockEntityType.Builder.of(supplier, validBlocks); } @Override public CreativeModeTab.Builder tabBuilder() { return FabricItemGroup.builder(); } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricPlatformHelper.java ================================================ package net.darkhax.bookshelf.fabric.impl.util; import net.darkhax.bookshelf.common.api.ModEntry; import net.darkhax.bookshelf.common.api.PhysicalSide; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.util.IPlatformHelper; import net.fabricmc.api.EnvType; import net.fabricmc.fabric.impl.gametest.FabricGameTestHelper; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.metadata.ModMetadata; import java.nio.file.Path; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; public class FabricPlatformHelper implements IPlatformHelper { private static final Supplier> LOADED_MODS = CachedSupplier.cache(() -> FabricLoader.getInstance().getAllMods().stream().map(mod -> { final ModMetadata meta = mod.getMetadata(); return new ModEntry(meta.getId(), meta.getName(), meta.getDescription(), meta.getVersion().getFriendlyString()); }).collect(Collectors.toSet())); @Override public Path getGamePath() { return FabricLoader.getInstance().getGameDir(); } @Override public Path getConfigPath() { return FabricLoader.getInstance().getConfigDir(); } @Override public Path getModsPath() { return this.getGamePath().resolve("mods"); } @Override public boolean isModLoaded(String modId) { return FabricLoader.getInstance().isModLoaded(modId); } @Override public boolean isDevelopmentEnvironment() { return FabricLoader.getInstance().isDevelopmentEnvironment(); } @Override public PhysicalSide getPhysicalSide() { return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT ? PhysicalSide.CLIENT : PhysicalSide.SERVER; } @Override public Set getLoadedMods() { return LOADED_MODS.get(); } @Override public boolean isTestingEnvironment() { return FabricGameTestHelper.ENABLED; } @Override public String getName() { return "Fabric"; } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricRegistryHelper.java ================================================ package net.darkhax.bookshelf.fabric.impl.util; import com.google.common.collect.Multimap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.registry.ContentProvider; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter; import net.darkhax.bookshelf.common.mixin.access.client.AccessorItemBlockRenderTypes; import net.darkhax.bookshelf.fabric.impl.data.FabricIngredient; import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.level.block.Block; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Supplier; public final class FabricRegistryHelper { private final ContentProvider content; private final RegistrationContext context; public FabricRegistryHelper(ContentProvider content) { this.content = content; this.context = new RegistrationContext(content.namespace()); if (content.canLoad()) { this.registerContent(); this.registerVillagerTrades(); this.registerCommands(); if (Services.PLATFORM.isPhysicalClient()) { this.registerClient(); } } else { Constants.LOG.debug("Content provider {} is disabled.", content); } } private void registerContent() { this.content.defineLoadConditions(new GenericRegistryAdapter<>(this.context, (id, val) -> LoadConditions.register(id, val.get()))); this.content.defineBlocks(new BlockRegistryAdapter(this.context, Registries.BLOCK, adapt(BuiltInRegistries.BLOCK))); this.context.getPlaceableBlocks().forEach((ref, factory) -> Registry.register(BuiltInRegistries.ITEM, ref.key().location(), factory.apply(ref.value().get()))); this.content.defineItems(new GameRegistryAdapter<>(this.context, Registries.ITEM, adapt(BuiltInRegistries.ITEM))); this.content.defineCreativeTabs(new CreativeModeTabAdapter(this.context, Registries.CREATIVE_MODE_TAB, adapt(BuiltInRegistries.CREATIVE_MODE_TAB))); this.content.defineIngredientTypes(new IngredientTypeAdapter(this.context, (id, value) -> CustomIngredientSerializer.register(adaptType(id, value.get())))); this.content.defineRecipeTypes(new RecipeTypeAdapter(this.context, Registries.RECIPE_TYPE, adapt(BuiltInRegistries.RECIPE_TYPE))); this.content.defineAttributes(new GameRegistryAdapter<>(this.context, Registries.ATTRIBUTE, adapt(BuiltInRegistries.ATTRIBUTE))); this.content.defineMobEffects(new GameRegistryAdapter<>(this.context, Registries.MOB_EFFECT, adapt(BuiltInRegistries.MOB_EFFECT))); this.content.defineCriteriaTriggers(new GameRegistryAdapter<>(this.context, Registries.TRIGGER_TYPE, adapt(BuiltInRegistries.TRIGGER_TYPES))); this.content.defineItemSubPredicates(new GameRegistryAdapter<>(this.context, Registries.ITEM_SUB_PREDICATE_TYPE, adapt(BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE))); this.content.defineEntities(new GameRegistryAdapter<>(this.context, Registries.ENTITY_TYPE, adapt(BuiltInRegistries.ENTITY_TYPE))); this.content.defineCatVariants(new GameRegistryAdapter<>(this.context, Registries.CAT_VARIANT, adapt(BuiltInRegistries.CAT_VARIANT))); this.content.definePotions(new GameRegistryAdapter<>(this.context, Registries.POTION, adapt(BuiltInRegistries.POTION))); this.content.definePotPatterns(new PotPatternAdapter(this.context, Registries.DECORATED_POT_PATTERN, adapt(BuiltInRegistries.DECORATED_POT_PATTERN))); this.content.defineItemComponents(new GameRegistryAdapter<>(this.context, Registries.DATA_COMPONENT_TYPE, adapt(BuiltInRegistries.DATA_COMPONENT_TYPE))); this.content.defineEnchantmentComponents(new GameRegistryAdapter<>(this.context, Registries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, adapt(BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE))); this.content.defineLootConditions(new GameRegistryAdapter<>(this.context, Registries.LOOT_CONDITION_TYPE, adapt(BuiltInRegistries.LOOT_CONDITION_TYPE))); this.content.defineLootFunctions(new GameRegistryAdapter<>(this.context, Registries.LOOT_FUNCTION_TYPE, adapt(BuiltInRegistries.LOOT_FUNCTION_TYPE))); this.content.defineBlockEntities(new GameRegistryAdapter<>(this.context, Registries.BLOCK_ENTITY_TYPE, adapt(BuiltInRegistries.BLOCK_ENTITY_TYPE))); this.content.defineRecipeSerializers(new GameRegistryAdapter<>(this.context, Registries.RECIPE_SERIALIZER, adapt(BuiltInRegistries.RECIPE_SERIALIZER))); this.content.defineLootEntryTypes(new LootEntryTypeAdapter(this.context, Registries.LOOT_POOL_ENTRY_TYPE, adapt(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE))); this.content.defineMenuType(new MenuTypeAdapter(this.context, (key, factory) -> Registry.register(BuiltInRegistries.MENU, key, new MenuType<>(factory.get()::create, FeatureFlags.VANILLA_SET)))); this.content.definePackets(new PacketAdapter(this.context, Services.NETWORK::register)); this.content.defineSounds(new SoundEventAdapter(this.context, Registries.SOUND_EVENT, adapt(BuiltInRegistries.SOUND_EVENT))); } @SuppressWarnings({"rawtypes", "unchecked"}) private void registerClient() { this.content.defineMenuScreens(new MenuScreenAdapter((id, factory) -> MenuScreens.register(id, (MenuScreens.ScreenConstructor) factory::create))); final Map blockRenderTypes = AccessorItemBlockRenderTypes.bookshelf$getBlockTypes(); this.content.defineBlockRenderTypes(new BlockRenderTypeAdapter(blockRenderTypes::put)); this.content.defineBlockRenderers(new BlockEntityRendererAdapter(BlockEntityRenderers::register)); } private void registerVillagerTrades() { final VillagerTradeAdapter register = new VillagerTradeAdapter(); this.content.defineTrades(register); for (Map.Entry> professionData : register.getVillagerTrades().entrySet()) { final Int2ObjectMap professionTrades = VillagerTrades.TRADES.computeIfAbsent(professionData.getKey(), profession -> new Int2ObjectOpenHashMap<>()); for (int merchantTier : professionData.getValue().keySet()) { final List tradesForTier = new ArrayList<>(Arrays.asList(professionTrades.getOrDefault(merchantTier, new VillagerTrades.ItemListing[0]))); tradesForTier.addAll(professionData.getValue().get(merchantTier)); professionTrades.put(merchantTier, tradesForTier.toArray(new VillagerTrades.ItemListing[0])); } } final List commonTrades = register.getCommonWanderingTrades(); if (!commonTrades.isEmpty()) { final List tradeData = new ArrayList<>(Arrays.asList(VillagerTrades.WANDERING_TRADER_TRADES.get(1))); tradeData.addAll(commonTrades); VillagerTrades.WANDERING_TRADER_TRADES.put(1, tradeData.toArray(new VillagerTrades.ItemListing[0])); } final List rareTrades = register.getRareWanderingTrades(); if (!rareTrades.isEmpty()) { final List tradeData = new ArrayList<>(Arrays.asList(VillagerTrades.WANDERING_TRADER_TRADES.get(2))); tradeData.addAll(rareTrades); VillagerTrades.WANDERING_TRADER_TRADES.put(2, tradeData.toArray(new VillagerTrades.ItemListing[0])); } } private void registerCommands() { CommandRegistrationCallback.EVENT.register(this.content::defineCommands); this.content.defineCommandArguments(new CommandArgumentAdapter(this.context, (rl, info) -> registerCommandArgument(rl, info.get()))); } @SuppressWarnings({"rawtypes", "unchecked"}) private static void registerCommandArgument(ResourceLocation key, CommandArgumentAdapter.TypeInfo type) { ArgumentTypeRegistry.registerArgumentType(key, type.argType(), type.typeIfo()); } @SuppressWarnings({"rawtypes", "unchecked"}) private static CustomIngredientSerializer adaptType(ResourceLocation id, IngredientTypeAdapter.IngredientType type) { return FabricIngredient.make(id, type.codec(), type.stream()); } private static BiConsumer, Supplier> adapt(Registry registry) { return (key, value) -> Registry.register(registry, key, value.get()); } } ================================================ FILE: fabric/src/main/java/net/darkhax/bookshelf/fabric/impl/util/FabricRenderHelper.java ================================================ package net.darkhax.bookshelf.fabric.impl.util; import com.mojang.blaze3d.vertex.PoseStack; import net.darkhax.bookshelf.common.api.util.IRenderHelper; import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandler; import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.material.FluidState; public class FabricRenderHelper implements IRenderHelper { @Override public void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay) { final FluidRenderHandler renderer = FluidRenderHandlerRegistry.INSTANCE.get(fluidState.getType()); if (renderer != null) { final int[] color = unpackARGB(renderer.getFluidColor(level, pos, fluidState)); // Correct Fabric API not supporting alpha. if (color[0] == 0) { color[0] = 255; } final TextureAtlasSprite sprite = renderer.getFluidSprites(level, pos, fluidState)[0]; renderBox(bufferSource.getBuffer(RenderType.translucent()), pose, sprite, light, overlay, color); } } } ================================================ FILE: fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.network.INetworkHandler ================================================ net.darkhax.bookshelf.fabric.impl.network.FabricNetworkHandler ================================================ FILE: fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IGameplayHelper ================================================ net.darkhax.bookshelf.fabric.impl.util.FabricGameplayHelper ================================================ FILE: fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IPlatformHelper ================================================ net.darkhax.bookshelf.fabric.impl.util.FabricPlatformHelper ================================================ FILE: fabric/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IRenderHelper ================================================ net.darkhax.bookshelf.fabric.impl.util.FabricRenderHelper ================================================ FILE: fabric/src/main/resources/bookshelf.fabric.mixins.json ================================================ { "required": true, "minVersion": "0.8", "package": "net.darkhax.bookshelf.fabric.mixin", "refmap": "${mod_id}.refmap.json", "compatibilityLevel": "JAVA_21", "mixins": [], "client": [], "server": [], "injectors": { "defaultRequire": 1 } } ================================================ FILE: fabric/src/main/resources/fabric.mod.json ================================================ { "schemaVersion": 1, "id": "${mod_id}", "version": "${version}", "name": "${mod_name}", "description": "${mod_description}", "authors": [ "${mod_author}" ], "contributors": [ "This project is made possible with Patreon support from players like you. Thank you!", "${patreon_pledges}" ], "contact": { "sources": "${mod_repo}", "issues": "${mod_repo}/issues", "homepage": "${curse_page}" }, "license": "${mod_license}", "icon": "logo_${mod_id}.png", "environment": "${mod_target_environment}", "entrypoints": { "main": [ "net.darkhax.bookshelf.fabric.impl.FabricMod" ], "client": [ "net.darkhax.bookshelf.fabric.impl.FabricModClient" ] }, "mixins": [ "${mod_id}.mixins.json", "${mod_id}.fabric.mixins.json" ], "depends": { "fabricloader": ">=${fabric_loader_version}", "fabric-api": "*", "minecraft": "${minecraft_version}", "java": ">=${java_version}" }, "custom": { "modmenu": { "links": { "modmenu.curseforge": "${curse_page}", "modmenu.modrinth": "${modrinth_page}", "modmenu.patreon": "${patreon_url}?${mod_id}" } } } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project version=21.1 group=net.darkhax.bookshelf java_version=21 # Mod mod_name=Bookshelf mod_author=Darkhax mod_id=bookshelf mod_license=LGPL 2.1 mod_description=Bookshelf is a library mod that provides code, frameworks, and utilities for other mods. Many mods make use of Bookshelf and are powered by its code. mod_repo=https://github.com/Darkhax-Minecraft/Bookshelf mod_docs=https://docs.darkhax.net/mods/bookshelf mod_item_icon=minecraft:bookshelf # Build Flags mod_client_only=false # Common minecraft_version=1.21.1 minecraft_version_range=[1.21.1, 1.22) neo_form_version=1.21.1-20240808.144430 parchment_minecraft=1.21 parchment_version=2024.07.28 jei_version=19.18.10.218 crt_version=21.0.3 # NeoForge neoforge_version=21.1.209 neoforge_loader_version_range=[4,) # Fabric fabric_version=0.116.6+1.21.1 fabric_loader_version=0.17.2 modmenu_version=11.0.2 # CurseForge curse_project=228525 curse_page=https://www.curseforge.com/minecraft/mc-mods/bookshelf # Modrinth modrinth_project=uy4Cnpcm modrinth_page=https://modrinth.com/mod/bookshelf-lib # Gradle org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: neoforge/build.gradle ================================================ plugins { id 'multiloader-loader' id 'net.neoforged.moddev' id 'net.darkhax.curseforgegradle' id 'com.modrinth.minotaur' } neoForge { version = neoforge_version parchment { minecraftVersion = parchment_minecraft mappingsVersion = parchment_version } runs { configureEach { systemProperty('neoforge.enabledGameTestNamespaces', mod_id) ideName = "NeoForge ${it.name.capitalize()} (${project.path})" } client { client() } server { server() } } mods { "${mod_id}" { sourceSet sourceSets.main } } } dependencies { if (project.hasProperty('jei_version')) { compileOnly("mezz.jei:jei-${minecraft_version}-common-api:${jei_version}") compileOnly("mezz.jei:jei-${minecraft_version}-neoforge-api:${jei_version}") runtimeOnly("mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}") } } // CurseForge task publishCurseForge(type: net.darkhax.curseforgegradle.TaskPublishCurseForge) { apiToken = rootProject.findProperty('curse_auth') var mainFile = upload(curse_project, jar) mainFile.changelogType = 'markdown' mainFile.changelog = rootProject.findProperty('mod_changelog') mainFile.addJavaVersion('Java 21') mainFile.releaseType = 'release' mainFile.addModLoader('NeoForge') if (rootProject.hasProperty('mod_client_only') && rootProject.findProperty('mod_client_only') == 'true') { mainFile.addGameVersion('Client') } else { mainFile.addGameVersion('Server', 'Client') } // Append Patreon Supporters var patreonInfo = rootProject.findProperty('patreon') if (patreonInfo) { mainFile.changelog += "\n\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\n\n${patreonInfo.pledgeLog}" } } // Modrinth modrinth { var patreonInfo = rootProject.findProperty('patreon') var changelogText = rootProject.findProperty('mod_changelog') if (patreonInfo) { changelogText += "\n\nThis project is made possible by [Patreon](${patreonInfo.campaignUrl}?${mod_id}) support from players like you. Thank you!\n\n${patreonInfo.pledgeLog}" } token.set(rootProject.findProperty('modrinth_auth')) projectId.set(modrinth_project) changelog = changelogText versionName.set("${mod_name}-neoforge-${minecraft_version}-${version}") versionType.set('release') loaders = ["neoforge"] gameVersions = ["${minecraft_version}"] uploadFile.set(tasks.jar) } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/NeoForgeMod.java ================================================ package net.darkhax.bookshelf.neoforge.impl; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.BookshelfMod; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.neoforge.impl.network.NeoForgeNetworkHandler; import net.darkhax.bookshelf.neoforge.impl.util.NeoForgeRegistryHelper; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; @Mod(Constants.MOD_ID) public class NeoForgeMod { public NeoForgeMod(IEventBus eventBus) { BookshelfMod.getInstance().init(); Services.CONTENT.get().forEach(NeoForgeRegistryHelper::new); if (Services.NETWORK instanceof NeoForgeNetworkHandler handler) { eventBus.addListener(handler::registerPayloadHandlers); } } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/data/NeoForgeIngredient.java ================================================ package net.darkhax.bookshelf.neoforge.impl.data; import com.mojang.serialization.MapCodec; import net.darkhax.bookshelf.common.api.data.ingredient.IngredientLogic; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.common.crafting.ICustomIngredient; import net.neoforged.neoforge.common.crafting.IngredientType; import net.neoforged.neoforge.registries.NeoForgeRegistries; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; import java.util.stream.Stream; public class NeoForgeIngredient> implements ICustomIngredient { private final T logic; private final Supplier> type; public NeoForgeIngredient(T logic, Supplier> type) { this.logic = logic; this.type = type; } @Override public boolean test(@NotNull ItemStack stack) { return this.logic.test(stack); } @NotNull @Override public Stream getItems() { return this.logic.getAllMatchingStacks().stream(); } @Override public boolean isSimple() { return !this.logic.requiresTesting(); } @NotNull @Override public IngredientType getType() { return this.type.get(); } public static > IngredientType> makeIngredientType(ResourceLocation id, MapCodec codec, StreamCodec stream) { final Supplier> typeLookup = () -> NeoForgeRegistries.INGREDIENT_TYPES.get(id); final MapCodec> ingredientCodec = codec.xmap(l -> new NeoForgeIngredient<>(l, typeLookup), i -> i.logic); final StreamCodec> ingredientStream = stream.map(l -> new NeoForgeIngredient<>(l, typeLookup), i -> i.logic); return new IngredientType<>(ingredientCodec, ingredientStream); } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/network/NeoForgeNetworkHandler.java ================================================ package net.darkhax.bookshelf.neoforge.impl.network; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import net.darkhax.bookshelf.common.api.network.INetworkHandler; import net.darkhax.bookshelf.common.api.network.IPacket; import net.darkhax.bookshelf.common.impl.Constants; import net.minecraft.client.Minecraft; import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; import net.neoforged.neoforge.network.handling.IPayloadHandler; import net.neoforged.neoforge.network.registration.PayloadRegistrar; import java.util.HashMap; import java.util.Map; import java.util.Objects; public class NeoForgeNetworkHandler implements INetworkHandler { private static final Map> PACKETS = new HashMap<>(); private static final Multimap> PACKETS_BY_NAMESPACE = HashMultimap.create(); @Override public void register(IPacket packet) { PACKETS.put(packet.type().id(), packet); PACKETS_BY_NAMESPACE.put(packet.type().id().getNamespace(), packet); } public void registerPayloadHandlers(RegisterPayloadHandlersEvent event) { for (String namespace : PACKETS_BY_NAMESPACE.keySet()) { final PayloadRegistrar registrar = event.registrar(namespace).optional(); for (IPacket packet : PACKETS_BY_NAMESPACE.get(namespace)) { final IPayloadHandler handler = (payload, ctx) -> packet.handle(ctx.player() instanceof ServerPlayer serverPlayer ? serverPlayer : null, !ctx.player().level().isClientSide, payload); switch (packet.destination()) { case SERVER_TO_CLIENT -> registrar.optional().commonToClient(packet.type(), packet.streamCodec(), handler); case CLIENT_TO_SERVER -> registrar.optional().commonToServer(packet.type(), packet.streamCodec(), handler); case BIDIRECTIONAL -> registrar.optional().commonBidirectional(packet.type(), packet.streamCodec(), handler); } } } } @Override public void sendToServer(T payload) { final ResourceLocation id = payload.type().id(); if (!PACKETS.containsKey(id)) { Constants.LOG.error("Attempted to send unregistered packet {} to the server.", id); throw new IllegalStateException("Attempted to send unregistered packet " + id + " to the server."); } if (Minecraft.getInstance().player == null) { Constants.LOG.error("Attempted to send packet {} to the server before a player instance is available.", id); throw new IllegalStateException("Attempted to send packet " + id + " to the server before a player instance is available."); } Objects.requireNonNull(Minecraft.getInstance().getConnection()).getConnection().send(new ServerboundCustomPayloadPacket(payload)); } @Override public void sendToPlayer(ServerPlayer recipient, T payload) { final ResourceLocation id = payload.type().id(); if (!PACKETS.containsKey(id)) { Constants.LOG.error("Attempted to send unregistered packet {} to player {}.", id, recipient); throw new IllegalStateException("Attempted to send unregistered packet " + id + " to player " + recipient); } PacketDistributor.sendToPlayer(recipient, payload); } @Override public boolean canSendPacket(ServerPlayer recipient, ResourceLocation payloadId) { return recipient.connection.hasChannel(payloadId); } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeGameplayHelper.java ================================================ package net.darkhax.bookshelf.neoforge.impl.util; import net.darkhax.bookshelf.common.api.util.IGameplayHelper; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.ItemHandlerHelper; import java.util.function.BiFunction; public class NeoForgeGameplayHelper implements IGameplayHelper { @Override public ItemStack getCraftingRemainder(ItemStack input) { final Item item = input.getItem(); return item.hasCraftingRemainingItem(input) ? item.getCraftingRemainingItem(input) : ItemStack.EMPTY; } @Override public ItemStack inventoryInsert(ServerLevel level, BlockPos pos, Direction side, ItemStack stack) { final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, side); return inventory != null ? ItemHandlerHelper.insertItemStacked(inventory, stack, false) : IGameplayHelper.super.inventoryInsert(level, pos, side, stack); } @Override public BlockEntityType.Builder blockEntityBuilder(BiFunction factory, Block... validBlocks) { BlockEntityType.BlockEntitySupplier supplier = factory::apply; return BlockEntityType.Builder.of(supplier, validBlocks); } @Override public CreativeModeTab.Builder tabBuilder() { return CreativeModeTab.builder(); } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgePlatformHelper.java ================================================ package net.darkhax.bookshelf.neoforge.impl.util; import net.darkhax.bookshelf.common.api.ModEntry; import net.darkhax.bookshelf.common.api.PhysicalSide; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.util.IPlatformHelper; import net.neoforged.fml.ModList; import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.gametest.GameTestHooks; import java.nio.file.Path; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; public class NeoForgePlatformHelper implements IPlatformHelper { private static final Supplier> LOADED_MODS = CachedSupplier.cache(() -> ModList.get().getMods().stream().map(mod -> new ModEntry(mod.getModId(), mod.getDisplayName(), mod.getDescription(), mod.getVersion().toString())).collect(Collectors.toSet())); @Override public Path getGamePath() { return FMLPaths.GAMEDIR.get(); } @Override public Path getConfigPath() { return FMLPaths.CONFIGDIR.get(); } @Override public Path getModsPath() { return FMLPaths.MODSDIR.get(); } @Override public boolean isModLoaded(String modId) { return ModList.get().isLoaded(modId); } @Override public boolean isDevelopmentEnvironment() { return !FMLLoader.isProduction(); } @Override public PhysicalSide getPhysicalSide() { return FMLEnvironment.dist.isClient() ? PhysicalSide.CLIENT : PhysicalSide.SERVER; } @Override public Set getLoadedMods() { return LOADED_MODS.get(); } @Override public boolean isTestingEnvironment() { return GameTestHooks.isGametestEnabled(); } @Override public String getName() { return "NeoForge"; } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeRegistryHelper.java ================================================ package net.darkhax.bookshelf.neoforge.impl.util; import com.google.common.collect.Multimap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.darkhax.bookshelf.common.api.data.conditions.LoadConditions; import net.darkhax.bookshelf.common.api.function.CachedSupplier; import net.darkhax.bookshelf.common.api.registry.ContentProvider; import net.darkhax.bookshelf.common.api.registry.RegistrationContext; import net.darkhax.bookshelf.common.api.registry.adapters.GameRegistryAdapter; import net.darkhax.bookshelf.common.api.registry.adapters.GenericRegistryAdapter; import net.darkhax.bookshelf.common.api.service.Services; import net.darkhax.bookshelf.common.impl.Constants; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockEntityRendererAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRegistryAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.BlockRenderTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CommandArgumentAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.CreativeModeTabAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.IngredientTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.LootEntryTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuScreenAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.MenuTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PacketAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.PotPatternAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.RecipeTypeAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.SoundEventAdapter; import net.darkhax.bookshelf.common.impl.registry.adapter.VillagerTradeAdapter; import net.darkhax.bookshelf.common.mixin.access.client.AccessorItemBlockRenderTypes; import net.darkhax.bookshelf.neoforge.impl.data.NeoForgeIngredient; import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.renderer.RenderType; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.ArgumentTypeInfos; import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.level.block.Block; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.ModList; import net.neoforged.fml.javafmlmod.FMLModContainer; import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.crafting.IngredientType; import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.village.VillagerTradesEvent; import net.neoforged.neoforge.event.village.WandererTradesEvent; import net.neoforged.neoforge.registries.NeoForgeRegistries; import net.neoforged.neoforge.registries.RegisterEvent; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; public final class NeoForgeRegistryHelper { private final ContentProvider content; private final RegistrationContext context; private final IEventBus modBus; public NeoForgeRegistryHelper(ContentProvider content) { this.content = content; this.context = new RegistrationContext(content.namespace()); this.modBus = getModBus(content.namespace()); if (this.content.canLoad()) { this.content.defineLoadConditions(new GenericRegistryAdapter<>(this.context, (id, val) -> LoadConditions.register(id, val.get()))); this.modBus.addListener(this::registerContent); this.setupTradeRegistration(); this.setupCommandRegistration(); this.content.definePackets(new PacketAdapter(this.context, Services.NETWORK::register)); if (Services.PLATFORM.isPhysicalClient()) { this.modBus.addListener(this::bindMenuScreens); this.modBus.addListener(this::registerRenderers); } } else { Constants.LOG.debug("Content provider {} is disabled.", content); } } private void registerContent(RegisterEvent event) { event.register(Registries.BLOCK, helper -> { this.content.defineBlocks(new BlockRegistryAdapter(this.context, Registries.BLOCK, adapt(helper))); if (Services.PLATFORM.isPhysicalClient()) { final Map blockRenderTypes = AccessorItemBlockRenderTypes.bookshelf$getBlockTypes(); this.content.defineBlockRenderTypes(new BlockRenderTypeAdapter(blockRenderTypes::put)); } }); event.register(Registries.ITEM, helper -> { this.context.getPlaceableBlocks().forEach((blockRef, builder) -> helper.register(blockRef.key().location(), builder.apply(blockRef.value().get()))); this.content.defineItems(new GameRegistryAdapter<>(this.context, Registries.ITEM, adapt(helper))); }); this.adaptRegistry(event, Registries.CREATIVE_MODE_TAB, this.content::defineCreativeTabs, CreativeModeTabAdapter::new); event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, helper -> this.content.defineIngredientTypes(new IngredientTypeAdapter(this.context, (id, value) -> helper.register(id, adaptType(id, value.get()))))); this.adaptRegistry(event, Registries.RECIPE_TYPE, this.content::defineRecipeTypes, RecipeTypeAdapter::new); this.adaptRegistry(event, Registries.ATTRIBUTE, this.content::defineAttributes); this.adaptRegistry(event, Registries.MOB_EFFECT, this.content::defineMobEffects); this.adaptRegistry(event, Registries.TRIGGER_TYPE, this.content::defineCriteriaTriggers); this.adaptRegistry(event, Registries.ITEM_SUB_PREDICATE_TYPE, this.content::defineItemSubPredicates); this.adaptRegistry(event, Registries.ENTITY_TYPE, this.content::defineEntities); this.adaptRegistry(event, Registries.CAT_VARIANT, this.content::defineCatVariants); this.adaptRegistry(event, Registries.POTION, this.content::definePotions); this.adaptRegistry(event, Registries.DECORATED_POT_PATTERN, this.content::definePotPatterns, PotPatternAdapter::new); this.adaptRegistry(event, Registries.DATA_COMPONENT_TYPE, this.content::defineItemComponents); this.adaptRegistry(event, Registries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, this.content::defineEnchantmentComponents); this.adaptRegistry(event, Registries.LOOT_CONDITION_TYPE, this.content::defineLootConditions); this.adaptRegistry(event, Registries.LOOT_FUNCTION_TYPE, this.content::defineLootFunctions); this.adaptRegistry(event, Registries.BLOCK_ENTITY_TYPE, this.content::defineBlockEntities); event.register(Registries.COMMAND_ARGUMENT_TYPE, helper -> this.content.defineCommandArguments(new CommandArgumentAdapter(this.context, (key, argType) -> registerCommandArgument(helper, key, argType.get())))); this.adaptRegistry(event, Registries.RECIPE_SERIALIZER, this.content::defineRecipeSerializers); this.adaptRegistry(event, Registries.LOOT_POOL_ENTRY_TYPE, this.content::defineLootEntryTypes, LootEntryTypeAdapter::new); event.register(Registries.MENU, helper -> this.content.defineMenuType(new MenuTypeAdapter(this.context, (key, factory) -> helper.register(key, new MenuType<>(factory.get()::create, FeatureFlags.VANILLA_SET))))); this.adaptRegistry(event, Registries.SOUND_EVENT, this.content::defineSounds, SoundEventAdapter::new); } private void setupCommandRegistration() { NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> this.content.defineCommands(event.getDispatcher(), event.getBuildContext(), event.getCommandSelection())); } private void setupTradeRegistration() { final CachedSupplier trades = CachedSupplier.cache(() -> { final VillagerTradeAdapter adapter = new VillagerTradeAdapter(); this.content.defineTrades(adapter); return adapter; }); NeoForge.EVENT_BUS.addListener(VillagerTradesEvent.class, event -> { final Multimap newTrades = trades.get().getVillagerTrades().get(event.getType()); if (newTrades != null && !newTrades.isEmpty()) { final Int2ObjectMap> tradeData = event.getTrades(); for (Map.Entry entry : newTrades.entries()) { tradeData.computeIfAbsent(entry.getKey(), ArrayList::new).add(entry.getValue()); } } }); NeoForge.EVENT_BUS.addListener(WandererTradesEvent.class, event -> { trades.get().getCommonWanderingTrades().forEach(event.getGenericTrades()::add); trades.get().getRareWanderingTrades().forEach(event.getRareTrades()::add); }); } @SuppressWarnings({"unchecked", "rawtypes"}) private void bindMenuScreens(RegisterMenuScreensEvent event) { final MenuScreenAdapter adapter = new MenuScreenAdapter((type, factory) -> event.register(type, (MenuScreens.ScreenConstructor) factory::create)); this.content.defineMenuScreens(adapter); } private void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { this.content.defineBlockRenderers(new BlockEntityRendererAdapter(event::registerBlockEntityRenderer)); } private void adaptRegistry(RegisterEvent event, ResourceKey> registry, Consumer> contentProvider) { this.adaptRegistry(event, registry, contentProvider, (GameRegistryAdapterFactory>) GameRegistryAdapter::new); } private > void adaptRegistry(RegisterEvent event, ResourceKey> registry, Consumer contentProvider, GameRegistryAdapterFactory adapterFactory) { event.register(registry, helper -> contentProvider.accept(adapterFactory.build(this.context, registry, adapt(helper)))); } private static BiConsumer, Supplier> adapt(RegisterEvent.RegisterHelper helper) { return (key, value) -> helper.register(key, value.get()); } private static BiConsumer> adaptGeneric(RegisterEvent.RegisterHelper helper) { return (key, value) -> helper.register(key, value.get()); } @SuppressWarnings({"rawtypes", "unchecked"}) private static IngredientType adaptType(ResourceLocation id, IngredientTypeAdapter.IngredientType type) { return NeoForgeIngredient.makeIngredientType(id, type.codec(), type.stream()); } @SuppressWarnings({"rawtypes", "unchecked"}) private static void registerCommandArgument(RegisterEvent.RegisterHelper> helper, ResourceLocation key, CommandArgumentAdapter.TypeInfo type) { helper.register(key, type.typeIfo()); ArgumentTypeInfos.registerByClass(type.argType(), type.typeIfo()); } private static IEventBus getModBus(String modid) { final ModContainer container = ModList.get().getModContainerById(modid).orElseThrow(() -> new IllegalArgumentException("Could not find mod '" + modid + "'.")); if (container instanceof FMLModContainer fmlContainer) { final IEventBus modEventBus = fmlContainer.getEventBus(); if (modEventBus != null) { return modEventBus; } throw new IllegalStateException("Mod '" + modid + "' does not have an event bus!"); } throw new IllegalStateException("Mod '" + modid + "' is not an FML mod!"); } @FunctionalInterface public interface GameRegistryAdapterFactory> { A build(RegistrationContext context, ResourceKey> registryKey, BiConsumer, Supplier> registryFunc); } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/impl/util/NeoForgeRenderHelper.java ================================================ package net.darkhax.bookshelf.neoforge.impl.util; import com.mojang.blaze3d.vertex.PoseStack; import net.darkhax.bookshelf.common.api.util.IRenderHelper; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import net.minecraft.world.level.material.FluidState; import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions; public class NeoForgeRenderHelper implements IRenderHelper { @Override public void renderFluidBox(PoseStack pose, FluidState fluidState, Level level, BlockPos pos, MultiBufferSource bufferSource, int light, int overlay) { final IClientFluidTypeExtensions fluidType = IClientFluidTypeExtensions.of(fluidState); final ResourceLocation texturePath = fluidType.getStillTexture(); final int[] color = unpackARGB(fluidType.getTintColor(fluidState, level, pos)); renderBox(bufferSource.getBuffer(RenderType.translucent()), pose, blockSprite(texturePath), light, overlay, color); } } ================================================ FILE: neoforge/src/main/java/net/darkhax/bookshelf/neoforge/mixin/access/gui/screen/AccessorMenuScreens.java ================================================ package net.darkhax.bookshelf.neoforge.mixin.access.gui.screen; import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(MenuScreens.class) public interface AccessorMenuScreens { @Invoker("register") static > void register(MenuType type, MenuScreens.ScreenConstructor factory) { throw new IllegalStateException("Mixin failed to apply."); } } ================================================ FILE: neoforge/src/main/resources/META-INF/neoforge.mods.toml ================================================ modLoader = "javafml" loaderVersion = "${neoforge_loader_version_range}" license = "${mod_license}" issueTrackerURL="${mod_repo}/issues" [[mods]] modId = "${mod_id}" version = "${version}" displayName = "${mod_name}" updateJSONURL = "https://updates.blamejared.com/get?n=${mod_id}&gv=${minecraft_version}&ml=${platform}" displayURL = "${curse_page}" logoFile="logo_${mod_id}.png" logoBlur = false credits = "This project is made possible with Patreon support from players like you. Thank you! ${patreon_pledges}" authors = "${mod_author}" description = "${mod_description}" [[mixins]] config = "${mod_id}.mixins.json" [[mixins]] config = "${mod_id}.neoforge.mixins.json" [[dependencies.${mod_id}]] modId = "neoforge" type = "required" versionRange = "[${neoforge_version},)" ordering = "NONE" side = "BOTH" [[dependencies.${mod_id}]] modId = "minecraft" type = "required" versionRange = "${minecraft_version_range}" ordering = "NONE" side = "BOTH" ================================================ FILE: neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.network.INetworkHandler ================================================ net.darkhax.bookshelf.neoforge.impl.network.NeoForgeNetworkHandler ================================================ FILE: neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IGameplayHelper ================================================ net.darkhax.bookshelf.neoforge.impl.util.NeoForgeGameplayHelper ================================================ FILE: neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IPlatformHelper ================================================ net.darkhax.bookshelf.neoforge.impl.util.NeoForgePlatformHelper ================================================ FILE: neoforge/src/main/resources/META-INF/services/net.darkhax.bookshelf.common.api.util.IRenderHelper ================================================ net.darkhax.bookshelf.neoforge.impl.util.NeoForgeRenderHelper ================================================ FILE: neoforge/src/main/resources/bookshelf.neoforge.mixins.json ================================================ { "required": true, "minVersion": "0.8", "package": "net.darkhax.bookshelf.neoforge.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ ], "client": [ "access.gui.screen.AccessorMenuScreens" ], "server": [], "injectors": { "defaultRequire": 1 } } ================================================ FILE: settings.gradle ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() exclusiveContent { forRepository { maven { name = 'Fabric' url = uri('https://maven.fabricmc.net') } } filter { includeGroupAndSubgroups('net.fabricmc') includeGroup('fabric-loom') } } exclusiveContent { forRepository { maven { name = 'Sponge Snapshots' url = uri("https://repo.spongepowered.org/repository/maven-public") } } filter { includeGroupAndSubgroups("org.spongepowered") } } } } plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } // This should match the folder name of the project, or else IDEA may complain (see https://youtrack.jetbrains.com/issue/IDEA-317606) rootProject.name = 'Bookshelf' include('common') include('neoforge') include('fabric')