Repository: Minestom/VanillaReimplementation Branch: master Commit: 05c3434ceed4 Files: 263 Total size: 1.1 MB Directory structure: gitextract_r6lz745n/ ├── .github/ │ └── CONTRIBUTING.md ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── block-update-system/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ ├── BlockUpdateFeature.java │ ├── blockupdatesystem/ │ │ ├── BlockUpdatable.java │ │ ├── BlockUpdateInfo.java │ │ └── BlockUpdateManager.java │ └── randomticksystem/ │ ├── RandomTickManager.java │ └── RandomTickable.java ├── blocks/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── blocks/ │ ├── VanillaBlockBehaviour.java │ ├── VanillaBlockLoot.java │ ├── VanillaBlocks.java │ ├── VanillaBlocksFeature.java │ └── behaviours/ │ ├── BedBlockBehaviour.java │ ├── CakeBlockBehaviour.java │ ├── ChestBlockBehaviour.java │ ├── ConcretePowderBlockBehaviour.java │ ├── EndPortalBlockBehaviour.java │ ├── EnderChestBlockBehaviour.java │ ├── FireBlockBehaviour.java │ ├── GravityBlockBehaviour.java │ ├── InventoryBlockBehaviour.java │ ├── JukeboxBlockBehaviour.java │ ├── NetherPortalBlockBehaviour.java │ ├── TNTBlockBehaviour.java │ ├── TrappedChestBlockBehaviour.java │ ├── chestlike/ │ │ ├── BlockInventory.java │ │ ├── BlockItems.java │ │ └── DoubleChestInventory.java │ ├── oxidisable/ │ │ ├── OxidatableBlockBehaviour.java │ │ ├── OxidatedBlockBehaviour.java │ │ ├── OxygenSensitive.java │ │ ├── WaxableBlockBehaviour.java │ │ └── WaxedBlockBehaviour.java │ └── recipe/ │ ├── BlastingFurnaceBehaviour.java │ ├── CampfireBehaviour.java │ ├── CraftingTableBehaviour.java │ ├── FurnaceBehaviour.java │ ├── SmithingTableBehaviour.java │ ├── SmokerBehaviour.java │ └── StonecutterBehaviour.java ├── build.gradle.kts ├── commands/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── commands/ │ ├── DifficultyCommand.java │ ├── ForceloadCommand.java │ ├── GamemodeCommand.java │ ├── HelpCommand.java │ ├── MeCommand.java │ ├── SaveAllCommand.java │ ├── StopCommand.java │ ├── VanillaCommands.java │ └── VanillaCommandsFeature.java ├── core/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── net/ │ │ │ └── minestom/ │ │ │ └── vanilla/ │ │ │ ├── VanillaRegistry.java │ │ │ ├── VanillaReimplementation.java │ │ │ ├── VanillaReimplementationImpl.java │ │ │ ├── dimensions/ │ │ │ │ └── VanillaDimensionTypes.java │ │ │ ├── events/ │ │ │ │ ├── BlastingFurnaceTickEvent.java │ │ │ │ ├── FurnaceTickEvent.java │ │ │ │ └── SmokerTickEvent.java │ │ │ ├── files/ │ │ │ │ ├── ByteArray.java │ │ │ │ ├── CacheFileSystem.java │ │ │ │ ├── DynamicFileSystem.java │ │ │ │ ├── FileSystem.java │ │ │ │ ├── FileSystemImpl.java │ │ │ │ ├── FileSystemMappers.java │ │ │ │ ├── FileSystemUtil.java │ │ │ │ ├── LazyFileSystem.java │ │ │ │ ├── MappedFileSystem.java │ │ │ │ └── PathFileSystem.java │ │ │ ├── instance/ │ │ │ │ ├── SetupVanillaInstanceEvent.java │ │ │ │ └── VanillaExplosion.java │ │ │ ├── inventory/ │ │ │ │ └── InventoryManipulation.java │ │ │ ├── logging/ │ │ │ │ ├── Color.java │ │ │ │ ├── Level.java │ │ │ │ ├── Loading.java │ │ │ │ ├── LoadingBar.java │ │ │ │ ├── LoadingImpl.java │ │ │ │ ├── Logger.java │ │ │ │ ├── LoggerImpl.java │ │ │ │ ├── LoggingLoadingBar.java │ │ │ │ ├── SLF4JCompatibilityLayer.java │ │ │ │ ├── SLF4JServiceProvider.java │ │ │ │ └── StatusUpdater.java │ │ │ ├── system/ │ │ │ │ ├── EnderChestSystem.java │ │ │ │ ├── NetherPortal.java │ │ │ │ ├── RayFastManager.java │ │ │ │ ├── ServerProperties.java │ │ │ │ └── nether/ │ │ │ │ ├── EntityEnterNetherPortalEvent.java │ │ │ │ ├── NetherPortalTeleportEvent.java │ │ │ │ └── NetherPortalUpdateEvent.java │ │ │ ├── tag/ │ │ │ │ └── Tags.java │ │ │ └── utils/ │ │ │ ├── DependencySorting.java │ │ │ ├── JavaUtils.java │ │ │ ├── MathUtils.java │ │ │ ├── MinestomUtils.java │ │ │ └── ZipUtils.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── org.tinylog.writers.Writer │ └── test/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── files/ │ └── FileSystemTests.java ├── crafting/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── crafting/ │ ├── CraftingFeature.java │ ├── CraftingRecipes.java │ └── Recipe.java ├── datapack/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── datapack/ │ └── Datapacks.java ├── datapack-loading/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── datapack/ │ ├── Datapack.java │ ├── DatapackLoader.java │ ├── DatapackLoadingFeature.java │ ├── DatapackUtils.java │ ├── advancement/ │ │ └── Advancement.java │ ├── dimension/ │ │ └── DimensionType.java │ ├── json/ │ │ ├── JsonUtils.java │ │ ├── ListLike.java │ │ └── Optional.java │ ├── loot/ │ │ ├── LootTable.java │ │ ├── NBTPath.java │ │ ├── NBTPathImpl.java │ │ ├── context/ │ │ │ ├── ContextGroups.java │ │ │ ├── LootContext.java │ │ │ ├── MappedTraitImpl.java │ │ │ ├── TraitImpl.java │ │ │ ├── Traits.java │ │ │ └── Util.java │ │ └── function/ │ │ ├── InBuiltLootFunctions.java │ │ ├── InBuiltPredicates.java │ │ ├── LootFunction.java │ │ └── Predicate.java │ ├── nbt/ │ │ └── NBTUtils.java │ ├── number/ │ │ ├── DoubleNumberProviders.java │ │ ├── IntNumberProviders.java │ │ └── NumberProvider.java │ ├── recipe/ │ │ └── Recipe.java │ ├── tags/ │ │ ├── ConditionsFor.java │ │ └── Tag.java │ ├── trims/ │ │ ├── TrimMaterial.java │ │ └── TrimPattern.java │ └── worldgen/ │ ├── Biome.java │ ├── BlockState.java │ ├── Carver.java │ ├── DensityFunction.java │ ├── DensityFunctions.java │ ├── FloatProvider.java │ ├── HeightProvider.java │ ├── LazyLoadedDensityFunction.java │ ├── NoiseSettings.java │ ├── Structure.java │ ├── VerticalAnchor.java │ ├── WorldgenContext.java │ ├── WorldgenRegistries.java │ ├── biome/ │ │ ├── BiomeSource.java │ │ ├── BiomeSources.java │ │ └── Climate.java │ ├── math/ │ │ ├── CubicSpline.java │ │ ├── NumberFunction.java │ │ └── SplineInterpolator.java │ ├── noise/ │ │ ├── BlendedNoise.java │ │ ├── ImprovedNoise.java │ │ ├── LazyLoadedNoise.java │ │ ├── Noise.java │ │ ├── NormalNoise.java │ │ ├── PerlinNoise.java │ │ └── SimplexNoise.java │ ├── random/ │ │ ├── LegacyRandom.java │ │ ├── MarsagliaPolarGaussian.java │ │ ├── WorldgenRandom.java │ │ ├── XoroshiroPositionalRandom.java │ │ └── XoroshiroRandom.java │ ├── storage/ │ │ ├── DoubleStorage.java │ │ ├── DoubleStorageCache.java │ │ ├── DoubleStorageCache2d.java │ │ └── DoubleStorageThreadLocalImpl.java │ └── util/ │ └── Util.java ├── datapack-tests/ │ ├── build.gradle.kts │ └── src/ │ └── test/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── datapack/ │ ├── loot/ │ │ ├── LootTableTestData.java │ │ └── LootTableTests.java │ └── worldgen/ │ ├── DF.java │ ├── DFVisualizer.java │ ├── DensityFunctionTests.java │ ├── NoiseTests.java │ └── RandomTests.java ├── entities/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── entities/ │ ├── FallingBlockEntity.java │ ├── MinestomEntitiesFeature.java │ └── PrimedTNTEntity.java ├── entity-meta/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── entitymeta/ │ └── EntityTags.java ├── fluid-simulation/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── io/ │ └── github/ │ └── togar2/ │ └── fluids/ │ ├── EmptyFluid.java │ ├── FlowableFluid.java │ ├── Fluid.java │ ├── FluidPlacementRule.java │ ├── FluidSimulationFeature.java │ ├── MinestomFluids.java │ ├── WaterBlockBreakEvent.java │ └── WaterFluid.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── instance-meta/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── instancemeta/ │ ├── InstanceMetaFeature.java │ └── tickets/ │ ├── TicketManager.java │ └── TicketUtils.java ├── item-placeables/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── itemplaceables/ │ └── ItemPlaceablesFeature.java ├── items/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── items/ │ ├── FlintAndSteelHandler.java │ ├── ItemManager.java │ ├── ItemsFeature.java │ ├── VanillaItemHandler.java │ └── VanillaItems.java ├── jitpack.yml ├── loot-table/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── loot/ │ ├── BlockExperience.java │ ├── LootContext.java │ ├── LootEntry.java │ ├── LootFeature.java │ ├── LootFunction.java │ ├── LootGenerator.java │ ├── LootNBT.java │ ├── LootNumber.java │ ├── LootPool.java │ ├── LootPredicate.java │ ├── LootScore.java │ ├── LootTable.java │ └── util/ │ ├── EnchantmentUtils.java │ ├── ListOperation.java │ ├── LootNumberRange.java │ ├── RelevantEntity.java │ ├── RelevantTarget.java │ ├── nbt/ │ │ ├── NBTPath.java │ │ ├── NBTReference.java │ │ └── NBTUtils.java │ └── predicate/ │ ├── DamageSourcePredicate.java │ ├── EntityPredicate.java │ ├── ItemPredicate.java │ └── LocationPredicate.java ├── mojang-data/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── io/ │ └── github/ │ └── pesto/ │ ├── MojangAssets.java │ └── MojangDataFeature.java ├── server/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── server/ │ ├── VanillaDebug.java │ ├── VanillaEvents.java │ └── VanillaServer.java ├── settings.gradle.kts ├── survival/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── minestom/ │ └── vanilla/ │ └── survival/ │ └── Survival.java └── world-generation/ ├── build.gradle.kts └── src/ └── main/ └── java/ └── net/ └── minestom/ └── vanilla/ └── generation/ ├── Aquifer.java ├── NoiseChunk.java ├── NoiseChunkGenerator.java ├── RandomState.java ├── SurfaceContext.java ├── SurfaceSystem.java ├── VanillaTestGenerator.java ├── VanillaWorldGenerationFeature.java └── VanillaWorldgen.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ ## How to contribute to Vanilla Reimplementation #### **Did you find a bug?** * Ensure that the associated system is implemented first. E.g. Don't report entities not spawning from spawners, if the spawner logic has not been implemented. * Open a new GitHub issue if it's not already reported. * Explain it clearly, with steps (or code) to reproduce it. #### **Did you write some code that fixes a bug?** * Open a new GitHub pull-request with the commits if it hasn't already been proposed. * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. #### **Do you intend to add a new feature or change an existing one?** * Do not open a pull-request on GitHub until you have collected positive feedback about the change from a maintainer. You can do this in the [minestom discord](https://discord.gg/pkFRvqB). #### **Do you have questions about the source code?** * Ask any question about the code in the associated discord channel. #### **Do you want to contribute to the Minestom documentation?** * Feel free to do so! Just make sure to conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification when editing the README.md. ## General Contribution Rules * By contributing to the Vanilla Reimplementation project your code/contribution will be licensed under the [Apache Version 2.0](../LICENSE) license. Minestom & VRI are community projects. We encourage you to contribute! :) Thanks! :heart: :heart: :heart: ~Minestom Community ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Java template # Compiled class file *.class */build/* build/* # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* # IntelliJ and Gradle files .idea/ out/ .gradle/ # Minestom /minecraft_data/ /.minestom_tmp/ # Vanilla-like server server.properties world/ /datapack-tests/mojang-data /mojang-data/1.20.4 /mojang-data/1.21.1 ================================================ FILE: .gitmodules ================================================ [submodule "vanilla_worldgen_example"] path = vanilla_worldgen_example url = https://github.com/slicedlime/examples/ [submodule "prismarine-minecraft-data"] path = prismarine-minecraft-data url = https://github.com/PrismarineJS/minecraft-data [submodule "Minestom"] path = Minestom url = https://github.com/Minestom/Minestom ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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 http://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. ================================================ FILE: README.md ================================================ # NOT READY FOR PRODUCTION Priority is currently on the core of Minestom (see below). This project has only a very limited list of features. Make sure to check out the project board [here](https://github.com/orgs/Minestom/projects/1). # About Minestom See [Minestom Project on GitHub](https://github.com/Minestom/Minestom) # Cloning `git clone --recurse-submodules https://github.com/Minestom/VanillaReimplementation` # How to use You can use this repo by finding the latest release [here](https://jitpack.io/#Minestom/VanillaReimplementation). After selecting your release, make sure to choose which modules (vanila features) you want. The "core" module is required. Everything else is optional and up to you. Once you have added your modules to your classpath, you can initiate vri in your server's startup using this snippet: `VanillaReimplementation vri = VanillaReimplementation.hook(MinecraftServer.process());`. See [here](https://github.com/Minestom/VanillaReimplementation/blob/93f29ab67ffff7d78e34b12ab5f00619109c84c7/server/src/main/java/net/minestom/vanilla/server/VanillaServer.java#L44) for an example. # How to contribute See [the github project](https://github.com/orgs/Minestom/projects/1) for a list of relevant tasks that need to be done. ================================================ FILE: block-update-system/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/BlockUpdateFeature.java ================================================ package net.minestom.vanilla; import net.kyori.adventure.key.Key; import net.minestom.vanilla.blockupdatesystem.BlockUpdateManager; import net.minestom.vanilla.logging.Loading; import net.minestom.vanilla.randomticksystem.RandomTickManager; import org.jetbrains.annotations.NotNull; public class BlockUpdateFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { Loading.start("Block Update Manager"); BlockUpdateManager.init(context); Loading.finish(); Loading.start("Random Tick Manager"); RandomTickManager.init(context); Loading.finish(); } @Override public @NotNull Key key() { return Key.key("vri:blockupdate"); } } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/blockupdatesystem/BlockUpdatable.java ================================================ package net.minestom.vanilla.blockupdatesystem; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; public interface BlockUpdatable { /** * Called when a block is updated. * * @param info The block update info. */ void blockUpdate(@NotNull Instance instance, @NotNull Point pos, @NotNull BlockUpdateInfo info); } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/blockupdatesystem/BlockUpdateInfo.java ================================================ package net.minestom.vanilla.blockupdatesystem; public interface BlockUpdateInfo { static DestroyBlock DESTROY_BLOCK() { return new DestroyBlock(); } static PlaceBlock PLACE_BLOCK() { return new PlaceBlock(); } static ChunkLoad CHUNK_LOAD() { return new ChunkLoad(); } static MoveBlock MOVE_BLOCK() { return new MoveBlock(); } record DestroyBlock() implements BlockUpdateInfo { } record PlaceBlock() implements BlockUpdateInfo { } record ChunkLoad() implements BlockUpdateInfo { } record MoveBlock() implements BlockUpdateInfo { } } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/blockupdatesystem/BlockUpdateManager.java ================================================ package net.minestom.vanilla.blockupdatesystem; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.InstanceChunkLoadEvent; import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.WeakHashMap; /** * A utility class used to facilitate block updates */ public class BlockUpdateManager { // Block update manager by instance private static final Map instance2BlockUpdateManager = Collections.synchronizedMap(new WeakHashMap<>()); // Block updatables private static final Short2ObjectMap blockUpdatables = new Short2ObjectOpenHashMap<>(); public static void registerUpdatable(short stateId, @NotNull BlockUpdatable updatable) { synchronized (blockUpdatables) { blockUpdatables.put(stateId, updatable); } } public static void init(@NotNull VanillaReimplementation.Feature.HookContext context) { EventNode eventNode = context.vri().process().eventHandler(); eventNode.addListener(InstanceTickEvent.class, BlockUpdateManager::instanceTick); eventNode.addListener(PlayerBlockBreakEvent.class, event -> BlockUpdateManager.from(event.getPlayer().getInstance()) .scheduleNeighborsUpdate(event.getBlockPosition(), BlockUpdateInfo.DESTROY_BLOCK()) ); eventNode.addListener(PlayerBlockPlaceEvent.class, event -> BlockUpdateManager.from(event.getPlayer().getInstance()) .scheduleNeighborsUpdate(event.getBlockPosition(), BlockUpdateInfo.PLACE_BLOCK()) ); eventNode.addListener(InstanceChunkLoadEvent.class, event -> { Chunk chunk = event.getChunk(); int minY = chunk.getMinSection() * Chunk.CHUNK_SECTION_SIZE; int maxY = chunk.getMaxSection() * Chunk.CHUNK_SECTION_SIZE; int minX = chunk.getChunkX() * Chunk.CHUNK_SIZE_X; int minZ = chunk.getChunkZ() * Chunk.CHUNK_SIZE_Z; Instance instance = event.getInstance(); BlockUpdateManager.from(instance); synchronized (blockUpdatables) { for (int x = minX; x < minX + Chunk.CHUNK_SIZE_X; x++) { for (int z = minZ; z < minZ + Chunk.CHUNK_SIZE_Z; z++) { for (int y = minY; y < maxY; y++) { Block block = chunk.getBlock(x, y, z); BlockUpdatable updatable = blockUpdatables.get((short) block.stateId()); if (updatable == null) continue; updatable.blockUpdate(instance, new Vec(x, y, z), BlockUpdateInfo.CHUNK_LOAD()); } } } } }); } private static void instanceTick(InstanceTickEvent event) { Instance instance = event.getInstance(); from(instance).tick(event.getDuration()); } public static @NotNull BlockUpdateManager from(@NotNull Instance instance) { return instance2BlockUpdateManager.computeIfAbsent(instance, BlockUpdateManager::new); } private final Map updateNeighbors = Collections.synchronizedMap(new LinkedHashMap<>()); private final BlockUpdateManager.UpdateHandler updateHandler; public BlockUpdateManager(@NotNull BlockUpdateManager.UpdateHandler updateHandler) { this.updateHandler = updateHandler; } private BlockUpdateManager(@NotNull Instance instance) { this.updateHandler = (pos, info) -> { if (instance.getBlock(pos).handler() instanceof BlockUpdatable updatable) { updatable.blockUpdate(instance, pos, info); } }; } public interface UpdateHandler { void update(@NotNull Point pos, @NotNull BlockUpdateInfo info); } // Public api methods /** * Schedules this position's neighbors to be updated next tick. */ public void scheduleNeighborsUpdate(Point pos, BlockUpdateInfo info) { updateNeighbors.put(pos, info); } // Public api methods end private void tick(int duration) { updateNeighbors(duration); } private void updateNeighbors(int duration) { if (updateNeighbors.isEmpty()) { return; } // Update all the neighbors for (Map.Entry entry : updateNeighbors.entrySet()) { Point pos = entry.getKey(); BlockUpdateInfo info = entry.getValue(); int x = pos.blockX(); int y = pos.blockY(); int z = pos.blockZ(); // For each surrounding block for (int offsetX = -1; offsetX < 2; offsetX++) { for (int offsetY = -1; offsetY < 2; offsetY++) { for (int offsetZ = -1; offsetZ < 2; offsetZ++) { // If block is not the original block if (offsetX == 0 && offsetY == 0 && offsetZ == 0) { continue; } // Get the block handler at the position Point blockPos = new Pos(x + offsetX, y + offsetY, z + offsetZ); updateHandler.update(blockPos, info); } } } } updateNeighbors.clear(); } } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/randomticksystem/RandomTickManager.java ================================================ package net.minestom.vanilla.randomticksystem; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.Map; import java.util.Random; import java.util.WeakHashMap; public class RandomTickManager { private static final @NotNull String RANDOM_TICK_SYSTEM_PROPERTY = "vri.gamerule.randomtickspeed"; private static final Map vri2managers = Collections.synchronizedMap(new WeakHashMap<>()); private static final Short2ObjectMap randomTickables = new Short2ObjectOpenHashMap<>(); private final VanillaReimplementation vri; private RandomTickManager(VanillaReimplementation vri) { this.vri = vri; } public static @NotNull RandomTickManager create(@NotNull VanillaReimplementation vri) { return vri2managers.computeIfAbsent(vri, RandomTickManager::new); } public static void init(VanillaReimplementation.Feature.@NotNull HookContext context) { RandomTickManager manager = create(context.vri()); context.vri().process().eventHandler().addListener(InstanceTickEvent.class, event -> { int randomTickCount = Integer.parseInt(System.getProperty(RANDOM_TICK_SYSTEM_PROPERTY, "3")); manager.handleInstanceTick(event, randomTickCount); }); } public static void registerRandomTickable(short stateId, RandomTickable randomTickable) { synchronized (randomTickables) { randomTickables.put(stateId, randomTickable); } } private void handleInstanceTick(InstanceTickEvent event, int randomTickCount) { Instance instance = event.getInstance(); Random instanceRandom = vri.random(instance); synchronized (randomTickables) { for (Chunk chunk : instance.getChunks()) { int minSection = chunk.getMinSection(); int maxSection = chunk.getMaxSection(); for (int section = minSection; section < maxSection; section++) { for (int i = 0; i < randomTickCount; i++) { randomTickSection(instanceRandom, instance, chunk, section); } } } } } private void randomTickSection(Random random, Instance instance, Chunk chunk, int minSection) { int minX = chunk.getChunkX() * Chunk.CHUNK_SIZE_X; int minZ = chunk.getChunkZ() * Chunk.CHUNK_SIZE_Z; int minY = minSection * Chunk.CHUNK_SECTION_SIZE; int x = minX + random.nextInt(Chunk.CHUNK_SIZE_X); int z = minZ + random.nextInt(Chunk.CHUNK_SIZE_Z); int y = minY + random.nextInt(Chunk.CHUNK_SECTION_SIZE); Point pos = new Vec(x, y, z); Block block = instance.getBlock(x, y, z); RandomTickable randomTickable = randomTickables.get((short) block.stateId()); if (randomTickable == null) return; randomTickable.randomTick(new RandomTick(instance, pos, block)); } private record RandomTick(Instance instance, Point position, Block block) implements RandomTickable.RandomTick {} } ================================================ FILE: block-update-system/src/main/java/net/minestom/vanilla/randomticksystem/RandomTickable.java ================================================ package net.minestom.vanilla.randomticksystem; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; public interface RandomTickable { void randomTick(@NotNull RandomTick randomTick); interface RandomTick { @NotNull Instance instance(); @NotNull Point position(); @NotNull Block block(); } } ================================================ FILE: blocks/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":block-update-system")) compileOnly(project(":entity-meta")) compileOnly(project(":datapack-loading")) } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/VanillaBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import org.jetbrains.annotations.NotNull; import java.util.Objects; /** * Represents a singular vanilla block's logic. e.g. white bed, cake, furnace, etc. */ public abstract class VanillaBlockBehaviour implements BlockHandler { protected final @NotNull VanillaBlocks.BlockContext context; protected final short baseBlock; protected final @NotNull Key key; protected VanillaBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { this.context = context; this.baseBlock = context.stateId(); this.key = Objects.requireNonNull(Block.fromStateId(context.stateId())).key(); } /** * DO NOT USE THIS. * @see #onPlace(VanillaPlacement) instead. */ @Override @Deprecated public void onPlace(@NotNull BlockHandler.Placement placement) { } public void onPlace(@NotNull VanillaPlacement placement) { } @Override public @NotNull Key getKey() { return key; } public interface VanillaPlacement { /** * @return the block that will be placed */ @NotNull Block blockToPlace(); /** * @return the instance that will be modified */ @NotNull Instance instance(); /** * @return the position of the block that will be placed */ @NotNull Point position(); /** * Overrides the current block to be placed. * * @param newBlock the new block to be placed */ void blockToPlace(@NotNull Block newBlock); interface HasPlayer { @NotNull Player player(); } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/VanillaBlockLoot.java ================================================ package net.minestom.vanilla.blocks; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.loot.LootTable; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.datapack.loot.function.LootFunction; import net.minestom.vanilla.datapack.loot.function.Predicate; import net.minestom.vanilla.files.FileSystem; import net.minestom.vanilla.logging.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Random; import java.util.function.Consumer; import java.util.random.RandomGenerator; public record VanillaBlockLoot(VanillaReimplementation vri, Datapack datapack) { private record LootEntry(@Nullable List functions, List items, double weight) { } public void spawnLoot(@NotNull PlayerBlockBreakEvent event) { String blockName = event.getBlock().key().value(); datapack.namespacedData().forEach((namespace, data) -> { FileSystem blocks = data.loot_tables().folder("blocks"); var lootTable = blocks.file(blockName + ".json"); if (lootTable == null) return; Block blockState = event.getBlock(); Point origin = event.getBlockPosition(); ItemStack tool = event.getPlayer().getItemInMainHand(); Player entity = event.getPlayer(); Block blockEntity = blockState.registry().blockEntity() == null ? null : blockState; Random random = vri.random(entity); LootContext context = new LootContext.Block(blockState, origin, tool, entity, blockEntity, null); List items = new ArrayList<>(); generateLootItems(lootTable, context, random, items::add); for (ItemStack item : items) { ItemEntity itemEntity = new ItemEntity(item); itemEntity.setInstance(entity.getInstance(), origin.add(0.5)); } }); } public List getLoot(LootTable lootTable, LootContext context) { return getLoot(lootTable, context, vri.random(0)); } public List getLoot(LootTable lootTable, LootContext context, Random random) { List items = new ArrayList<>(); generateLootItems(lootTable, context, random, items::add); return items; } private void generateLootItems(LootTable lootTable, LootContext context, Random random, Consumer out) { if (lootTable.pools() == null) return; // TODO: handle random_sequence for (LootTable.Pool pool : lootTable.pools()) { // Ensure all conditions are met if (fails(pool.conditions(), context)) continue; int rolls = pool.rolls().asInt().apply(() -> random); // collect all of the loot entries List entries = new ArrayList<>(); for (LootTable.Pool.Entry entry : pool.entries()) { // Ensure all conditions are met if (fails(entry.conditions(), context)) continue; // now we can add the entries addEntries(context, entry, itemGenerator -> { double weight = itemGenerator.weight() == null ? 1 : Objects.requireNonNull(itemGenerator.weight()).asDouble().apply(() -> random); var lootEntries = itemGenerator.apply(datapack, context); for (List lootEntryItems : lootEntries) { entries.add(new LootEntry(itemGenerator.functions(), lootEntryItems, weight)); } }); } // if there is no entries, we can skip this pool if (entries.isEmpty()) continue; // now that we have all the entries, we can roll for them double totalWeight = entries.stream().mapToDouble(LootEntry::weight).sum(); for (int i = 0; i < rolls; i++) { LootEntry chosenLootEntry = null; double roll = random.nextDouble() * totalWeight; for (LootEntry lootEntry : entries) { roll -= lootEntry.weight(); if (roll <= 0) { chosenLootEntry = lootEntry; break; } } Objects.requireNonNull(chosenLootEntry); // we now have the loot entry, we need to apply the loot functions LootEntry finalChosenLootEntry = chosenLootEntry; chosenLootEntry.items() .stream() // loot entry functions .map(item -> { if (finalChosenLootEntry.functions() == null) return item; for (LootFunction function : finalChosenLootEntry.functions()) { item = function.apply(new LootFunctionContext(random, item, context)); } return item; }) // pool functions .map(item -> { if (pool.functions() == null) return item; for (LootFunction function : pool.functions()) { item = function.apply(new LootFunctionContext(random, item, context)); } return item; }) // table functions .map(item -> { if (lootTable.functions() == null) return item; for (LootFunction function : lootTable.functions()) { item = function.apply(new LootFunctionContext(random, item, context)); } return item; }) // add all of the items to the list .forEach(out); } } } private record LootFunctionContext(RandomGenerator random, ItemStack itemStack, LootContext context) implements LootFunction.Context { @Override public @Nullable T get(Trait trait) { return context.get(trait); } } private static boolean fails(@Nullable List predicates, LootContext context) { if (predicates == null) return false; for (Predicate predicate : predicates) { if (!predicate.test(context)) { return true; } } return false; } private void addEntries(LootContext context, LootTable.Pool.Entry entry, Consumer out) { if (fails(entry.conditions(), context)) return; switch (entry.type().toString()) { case "minecraft:item", "minecraft:tag", "minecraft:dynamic", "minecraft:empty" -> out.accept((LootTable.Pool.Entry.ItemGenerator) entry); case "minecraft:loot_table" -> // TODO: recursive loot tables Logger.debug("Recursive loot tables are not yet supported"); case "minecraft:group" -> { LootTable.Pool.Entry.Group group = (LootTable.Pool.Entry.Group) entry; for (LootTable.Pool.Entry groupEntry : group.children()) { addEntries(context, groupEntry, out); } } case "minecraft:alternatives" -> { LootTable.Pool.Entry.Alternatives alternatives = (LootTable.Pool.Entry.Alternatives) entry; for (LootTable.Pool.Entry alternative : alternatives.children()) { if (fails(alternative.conditions(), context)) continue; addEntries(context, alternative, out); break; } } case "minecraft:sequence" -> { LootTable.Pool.Entry.Sequence sequence = (LootTable.Pool.Entry.Sequence) entry; for (LootTable.Pool.Entry alternative : sequence.children()) { if (fails(alternative.conditions(), context)) break; addEntries(context, alternative, out); } } } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/VanillaBlocks.java ================================================ package net.minestom.vanilla.blocks; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.Event; import net.minestom.server.event.EventListener; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.blocks.behaviours.*; import net.minestom.vanilla.blocks.behaviours.oxidisable.OxidatableBlockBehaviour; import net.minestom.vanilla.blocks.behaviours.oxidisable.WaxedBlockBehaviour; import net.minestom.vanilla.blocks.behaviours.recipe.*; import net.minestom.vanilla.blockupdatesystem.BlockUpdatable; import net.minestom.vanilla.blockupdatesystem.BlockUpdateManager; import net.minestom.vanilla.datapack.DatapackLoadingFeature; import net.minestom.vanilla.randomticksystem.RandomTickManager; import net.minestom.vanilla.randomticksystem.RandomTickable; import org.jetbrains.annotations.NotNull; import java.util.Objects; /** * All blocks available in the vanilla reimplementation */ public enum VanillaBlocks { SAND(Block.SAND, GravityBlockBehaviour::new), RED_SAND(Block.RED_SAND, GravityBlockBehaviour::new), GRAVEL(Block.GRAVEL, GravityBlockBehaviour::new), // Start of concrete powders WHITE_CONCRETE_POWDER(Block.WHITE_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.WHITE_CONCRETE)), BLACK_CONCRETE_POWDER(Block.BLACK_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.BLACK_CONCRETE)), LIGHT_BLUE_CONCRETE_POWDER(Block.LIGHT_BLUE_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.LIGHT_BLUE_CONCRETE)), BLUE_CONCRETE_POWDER(Block.BLUE_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.BLUE_CONCRETE)), RED_CONCRETE_POWDER(Block.RED_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.RED_CONCRETE)), GREEN_CONCRETE_POWDER(Block.GREEN_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.GREEN_CONCRETE)), YELLOW_CONCRETE_POWDER(Block.YELLOW_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.YELLOW_CONCRETE)), PURPLE_CONCRETE_POWDER(Block.PURPLE_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.PURPLE_CONCRETE)), MAGENTA_CONCRETE_POWDER(Block.MAGENTA_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.MAGENTA_CONCRETE)), CYAN_CONCRETE_POWDER(Block.CYAN_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.CYAN_CONCRETE)), PINK_CONCRETE_POWDER(Block.PINK_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.PINK_CONCRETE)), GRAY_CONCRETE_POWDER(Block.GRAY_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.GRAY_CONCRETE)), LIGHT_GRAY_CONCRETE_POWDER(Block.LIGHT_GRAY_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.LIGHT_GRAY_CONCRETE)), ORANGE_CONCRETE_POWDER(Block.ORANGE_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.ORANGE_CONCRETE)), BROWN_CONCRETE_POWDER(Block.BROWN_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.BROWN_CONCRETE)), LIME_CONCRETE_POWDER(Block.LIME_CONCRETE_POWDER, (context) -> new ConcretePowderBlockBehaviour(context, Block.LIME_CONCRETE)), // End of concrete powders // Start of oxidisable copper // Blocks COPPER_BLOCK(Block.COPPER_BLOCK, (context) -> new OxidatableBlockBehaviour(context, Block.COPPER_BLOCK, Block.EXPOSED_COPPER, Block.WAXED_COPPER_BLOCK, 0)), EXPOSED_COPPER(Block.EXPOSED_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.COPPER_BLOCK, Block.WEATHERED_COPPER, Block.WAXED_EXPOSED_COPPER, 1)), WEATHERED_COPPER(Block.WEATHERED_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.EXPOSED_COPPER, Block.OXIDIZED_COPPER, Block.WAXED_WEATHERED_COPPER, 2)), OXIDIZED_COPPER(Block.OXIDIZED_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.WEATHERED_COPPER, Block.OXIDIZED_COPPER, Block.WAXED_OXIDIZED_COPPER, 3)), // Cut Blocks CUT_COPPER(Block.CUT_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER, Block.EXPOSED_CUT_COPPER, Block.WAXED_CUT_COPPER, 0)), EXPOSED_CUT_COPPER(Block.EXPOSED_CUT_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER, Block.WEATHERED_CUT_COPPER, Block.WAXED_EXPOSED_CUT_COPPER, 1)), WEATHERED_CUT_COPPER(Block.WEATHERED_CUT_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.EXPOSED_CUT_COPPER, Block.OXIDIZED_CUT_COPPER, Block.WAXED_WEATHERED_CUT_COPPER, 2)), OXIDIZED_CUT_COPPER(Block.OXIDIZED_CUT_COPPER, (context) -> new OxidatableBlockBehaviour(context, Block.WEATHERED_CUT_COPPER, Block.OXIDIZED_CUT_COPPER, Block.WAXED_OXIDIZED_CUT_COPPER, 3)), // Stairs CUT_COPPER_STAIRS(Block.CUT_COPPER_STAIRS, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER_STAIRS, Block.EXPOSED_CUT_COPPER_STAIRS, Block.WAXED_CUT_COPPER_STAIRS, 0)), EXPOSED_CUT_COPPER_STAIRS(Block.EXPOSED_CUT_COPPER_STAIRS, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER_STAIRS, Block.WEATHERED_CUT_COPPER_STAIRS, Block.WAXED_EXPOSED_CUT_COPPER_STAIRS, 1)), WEATHERED_CUT_COPPER_STAIRS(Block.WEATHERED_CUT_COPPER_STAIRS, (context) -> new OxidatableBlockBehaviour(context, Block.EXPOSED_CUT_COPPER_STAIRS, Block.OXIDIZED_CUT_COPPER_STAIRS, Block.WAXED_WEATHERED_CUT_COPPER_STAIRS, 2)), OXIDIZED_CUT_COPPER_STAIRS(Block.OXIDIZED_CUT_COPPER_STAIRS, (context) -> new OxidatableBlockBehaviour(context, Block.WEATHERED_CUT_COPPER_STAIRS, Block.OXIDIZED_CUT_COPPER_STAIRS, Block.WAXED_OXIDIZED_CUT_COPPER_STAIRS, 3)), // Slabs CUT_COPPER_SLAB(Block.CUT_COPPER_SLAB, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER_SLAB, Block.EXPOSED_CUT_COPPER_SLAB, Block.WAXED_CUT_COPPER_SLAB, 0)), EXPOSED_CUT_COPPER_SLAB(Block.EXPOSED_CUT_COPPER_SLAB, (context) -> new OxidatableBlockBehaviour(context, Block.CUT_COPPER_SLAB, Block.WEATHERED_CUT_COPPER_SLAB, Block.WAXED_EXPOSED_CUT_COPPER_SLAB, 1)), WEATHERED_CUT_COPPER_SLAB(Block.WEATHERED_CUT_COPPER_SLAB, (context) -> new OxidatableBlockBehaviour(context, Block.EXPOSED_CUT_COPPER_SLAB, Block.OXIDIZED_CUT_COPPER_SLAB, Block.WAXED_WEATHERED_CUT_COPPER_SLAB, 2)), OXIDIZED_CUT_COPPER_SLAB(Block.OXIDIZED_CUT_COPPER_SLAB, (context) -> new OxidatableBlockBehaviour(context, Block.WEATHERED_CUT_COPPER_SLAB, Block.OXIDIZED_CUT_COPPER_SLAB, Block.WAXED_OXIDIZED_CUT_COPPER_SLAB, 3)), // End of copper // Start of waxed copper // Blocks WAXED_COPPER_BLOCK(Block.WAXED_COPPER_BLOCK, (context) -> new WaxedBlockBehaviour(context, Block.COPPER_BLOCK, 0)), WAXED_EXPOSED_COPPER(Block.WAXED_EXPOSED_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.EXPOSED_COPPER, 1)), WAXED_WEATHERED_COPPER(Block.WAXED_WEATHERED_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.WEATHERED_COPPER, 2)), WAXED_OXIDIZED_COPPER(Block.WAXED_OXIDIZED_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.OXIDIZED_COPPER, 3)), // Cut Blocks WAXED_CUT_COPPER(Block.WAXED_CUT_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.CUT_COPPER, 0)), WAXED_EXPOSED_CUT_COPPER(Block.WAXED_EXPOSED_CUT_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.EXPOSED_CUT_COPPER, 1)), WAXED_WEATHERED_CUT_COPPER(Block.WAXED_WEATHERED_CUT_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.WEATHERED_CUT_COPPER, 2)), WAXED_OXIDIZED_CUT_COPPER(Block.WAXED_OXIDIZED_CUT_COPPER, (context) -> new WaxedBlockBehaviour(context, Block.OXIDIZED_CUT_COPPER, 3)), // Stairs WAXED_CUT_COPPER_STAIRS(Block.WAXED_CUT_COPPER_STAIRS, (context) -> new WaxedBlockBehaviour(context, Block.CUT_COPPER_STAIRS, 0)), WAXED_EXPOSED_CUT_COPPER_STAIRS(Block.WAXED_EXPOSED_CUT_COPPER_STAIRS, (context) -> new WaxedBlockBehaviour(context, Block.EXPOSED_CUT_COPPER_STAIRS, 1)), WAXED_WEATHERED_CUT_COPPER_STAIRS(Block.WAXED_WEATHERED_CUT_COPPER_STAIRS, (context) -> new WaxedBlockBehaviour(context, Block.WEATHERED_CUT_COPPER_STAIRS, 2)), WAXED_OXIDIZED_CUT_COPPER_STAIRS(Block.WAXED_OXIDIZED_CUT_COPPER_STAIRS, (context) -> new WaxedBlockBehaviour(context, Block.OXIDIZED_CUT_COPPER_STAIRS, 3)), // Slabs WAXED_CUT_COPPER_SLAB(Block.WAXED_CUT_COPPER_SLAB, (context) -> new WaxedBlockBehaviour(context, Block.CUT_COPPER_SLAB, 0)), WAXED_EXPOSED_CUT_COPPER_SLAB(Block.WAXED_EXPOSED_CUT_COPPER_SLAB, (context) -> new WaxedBlockBehaviour(context, Block.EXPOSED_CUT_COPPER_SLAB, 1)), WAXED_WEATHERED_CUT_COPPER_SLAB(Block.WAXED_WEATHERED_CUT_COPPER_SLAB, (context) -> new WaxedBlockBehaviour(context, Block.WEATHERED_CUT_COPPER_SLAB, 2)), WAXED_OXIDIZED_CUT_COPPER_SLAB(Block.WAXED_OXIDIZED_CUT_COPPER_SLAB, (context) -> new WaxedBlockBehaviour(context, Block.OXIDIZED_CUT_COPPER_SLAB, 3)), // End of waxed copper // Start of beds WHITE_BED(Block.WHITE_BED, BedBlockBehaviour::new), BLACK_BED(Block.BLACK_BED, BedBlockBehaviour::new), LIGHT_BLUE_BED(Block.LIGHT_BLUE_BED, BedBlockBehaviour::new), BLUE_BED(Block.BLUE_BED, BedBlockBehaviour::new), RED_BED(Block.RED_BED, BedBlockBehaviour::new), GREEN_BED(Block.GREEN_BED, BedBlockBehaviour::new), YELLOW_BED(Block.YELLOW_BED, BedBlockBehaviour::new), PURPLE_BED(Block.PURPLE_BED, BedBlockBehaviour::new), MAGENTA_BED(Block.MAGENTA_BED, BedBlockBehaviour::new), CYAN_BED(Block.CYAN_BED, BedBlockBehaviour::new), PINK_BED(Block.PINK_BED, BedBlockBehaviour::new), GRAY_BED(Block.GRAY_BED, BedBlockBehaviour::new), LIGHT_GRAY_BED(Block.LIGHT_GRAY_BED, BedBlockBehaviour::new), ORANGE_BED(Block.ORANGE_BED, BedBlockBehaviour::new), BROWN_BED(Block.BROWN_BED, BedBlockBehaviour::new), LIME_BED(Block.LIME_BED, BedBlockBehaviour::new), // End of beds FIRE(Block.FIRE, FireBlockBehaviour::new), NETHER_PORTAL(Block.NETHER_PORTAL, NetherPortalBlockBehaviour::new), END_PORTAL(Block.END_PORTAL, EndPortalBlockBehaviour::new), TNT(Block.TNT, TNTBlockBehaviour::new), CHEST(Block.CHEST, ChestBlockBehaviour::new), TRAPPED_CHEST(Block.TRAPPED_CHEST, TrappedChestBlockBehaviour::new), ENDER_CHEST(Block.ENDER_CHEST, EnderChestBlockBehaviour::new), JUKEBOX(Block.JUKEBOX, JukeboxBlockBehaviour::new), // recipes CRAFTING_TABLE(Block.CRAFTING_TABLE, CraftingTableBehaviour::new), FURNACE(Block.FURNACE, FurnaceBehaviour::new), SMOKER(Block.SMOKER, SmokerBehaviour::new), BLAST_FURNACE(Block.BLAST_FURNACE, BlastingFurnaceBehaviour::new), STONE_CUTTER(Block.STONECUTTER, StonecutterBehaviour::new), CAMPFIRE(Block.CAMPFIRE, CampfireBehaviour::new), SOUL_CAMPFIRE(Block.SOUL_CAMPFIRE, CampfireBehaviour::new), SMITHING_TABLE(Block.SMITHING_TABLE, SmithingTableBehaviour::new), // Start of cakes CAKE(Block.CAKE, CakeBlockBehaviour::new), CANDLE_CAKE(Block.CANDLE_CAKE, CakeBlockBehaviour::new), WHITE_CANDLE_CAKE(Block.WHITE_CANDLE_CAKE, CakeBlockBehaviour::new), ORANGE_CANDLE_CAKE(Block.ORANGE_CANDLE_CAKE, CakeBlockBehaviour::new), MAGENTA_CANDLE_CAKE(Block.MAGENTA_CANDLE_CAKE, CakeBlockBehaviour::new), LIGHT_BLUE_CANDLE_CAKE(Block.LIGHT_BLUE_CANDLE_CAKE, CakeBlockBehaviour::new), YELLOW_CANDLE_CAKE(Block.YELLOW_CANDLE_CAKE, CakeBlockBehaviour::new), LIME_CANDLE_CAKE(Block.LIME_CANDLE_CAKE, CakeBlockBehaviour::new), PINK_CANDLE_CAKE(Block.PINK_CANDLE_CAKE, CakeBlockBehaviour::new), GRAY_CANDLE_CAKE(Block.GRAY_CANDLE_CAKE, CakeBlockBehaviour::new), LIGHT_GRAY_CANDLE_CAKE(Block.LIGHT_GRAY_CANDLE_CAKE, CakeBlockBehaviour::new), CYAN_CANDLE_CAKE(Block.CYAN_CANDLE_CAKE, CakeBlockBehaviour::new), PURPLE_CANDLE_CAKE(Block.PURPLE_CANDLE_CAKE, CakeBlockBehaviour::new), BLUE_CANDLE_CAKE(Block.BLUE_CANDLE_CAKE, CakeBlockBehaviour::new), BROWN_CANDLE_CAKE(Block.BROWN_CANDLE_CAKE, CakeBlockBehaviour::new), GREEN_CANDLE_CAKE(Block.GREEN_CANDLE_CAKE, CakeBlockBehaviour::new), BLACK_CANDLE_CAKE(Block.BLACK_CANDLE_CAKE, CakeBlockBehaviour::new) // End of cakes ; private final short stateId; private final @NotNull Context2Handler context2handler; VanillaBlocks(@NotNull Block minestomBlock, @NotNull Context2Handler context2handler) { this.stateId = (short) minestomBlock.stateId(); this.context2handler = context -> { if (context.stateId() != minestomBlock.stateId()) { throw new IllegalStateException("Block registry mismatch. Registered block: " + minestomBlock.stateId() + " != Given block:" + context.stateId()); } return context2handler.apply(context); }; } interface Context2Handler { @NotNull VanillaBlockBehaviour apply(@NotNull BlockContext context); } /** * Used to provide context for creating block handlers */ public interface BlockContext { short stateId(); @NotNull VanillaReimplementation vri(); } /** * Creates a block handler from the context * * @param context the context * @return the block handler */ public @NotNull VanillaBlockBehaviour create(@NotNull BlockContext context) { return context2handler.apply(context); } /** * Register all vanilla blocks. ConnectionManager will handle replacing the basic * block with its custom variant. * * @param vri the vanilla reimplementation object */ public static void registerAll(@NotNull VanillaReimplementation vri) { EventNode events = EventNode.all("vanilla-blocks"); // block loot VanillaBlockLoot loot = new VanillaBlockLoot(vri, vri.feature(DatapackLoadingFeature.class).current()); events.addListener(EventListener.builder(PlayerBlockBreakEvent.class) .filter(event -> !event.isCancelled()) .filter(event -> event.getPlayer().getGameMode() != GameMode.CREATIVE) .handler(loot::spawnLoot) .build()); Short2ObjectMap stateId2behaviour = new Short2ObjectOpenHashMap<>(); for (VanillaBlocks vb : values()) { BlockContext context = new BlockContext() { @Override public short stateId() { return vb.stateId; } @Override public @NotNull VanillaReimplementation vri() { return vri; } }; VanillaBlockBehaviour behaviour = vb.context2handler.apply(context); for (Block possibleState : Objects.requireNonNull(Block.fromStateId(vb.stateId)).possibleStates()) { short possibleStateId = (short) possibleState.stateId(); stateId2behaviour.put(possibleStateId, behaviour); } if (behaviour instanceof BlockUpdatable updatable) BlockUpdateManager.registerUpdatable(vb.stateId, updatable); if (behaviour instanceof RandomTickable randomTickable) RandomTickManager.registerRandomTickable(vb.stateId, randomTickable); } registerEvents(events, stateId2behaviour); vri.process().eventHandler().addChild(events); } private static void registerEvents(EventNode node, Short2ObjectMap behaviours) { node.addListener(EventListener.builder(PlayerBlockPlaceEvent.class) .filter(event -> behaviours.containsKey((short) event.getBlock().stateId())) .handler(event -> { short stateId = (short) event.getBlock().stateId(); Block block = Objects.requireNonNull(Block.fromStateId(stateId)); var behaviour = behaviours.get(stateId); behaviour.onPlace(new PlayerPlacement(event)); Block blockToPlace = event.getBlock(); if (blockToPlace.compare(block)) { event.setBlock(blockToPlace.withHandler(behaviour)); } }) .build()); } private record PlayerPlacement(PlayerBlockPlaceEvent event) implements VanillaBlockBehaviour.VanillaPlacement, VanillaBlockBehaviour.VanillaPlacement.HasPlayer { @Override public @NotNull Block blockToPlace() { return event.getBlock(); } @Override public @NotNull Instance instance() { return event.getInstance(); } @Override public @NotNull Point position() { return event.getBlockPosition(); } @Override public void blockToPlace(@NotNull Block newBlock) { event.setBlock(newBlock); } @Override public @NotNull Player player() { return event.getPlayer(); } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/VanillaBlocksFeature.java ================================================ package net.minestom.vanilla.blocks; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.BlockUpdateFeature; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.datapack.DatapackLoadingFeature; import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; public class VanillaBlocksFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { VanillaReimplementation vri = context.vri(); VanillaBlocks.registerAll(vri); vri.process().eventHandler().addListener(PlayerBlockPlaceEvent.class, event -> { Block block = event.getBlock(); Instance instance = event.getInstance(); Point position = event.getBlockPosition(); AtomicReference blockToPlace = new AtomicReference<>(block); if (block.handler() instanceof VanillaBlockBehaviour vanillaHandler) { // Create the new placement object VanillaBlockBehaviour.VanillaPlacement placement = new PlacementImpl(blockToPlace, instance, position); vanillaHandler.onPlace(placement); } event.setBlock(blockToPlace.get()); }); } private record PlacementImpl(AtomicReference blockToPlaceRef, Instance instance, Point position) implements VanillaBlockBehaviour.VanillaPlacement { @Override public @NotNull Block blockToPlace() { return blockToPlaceRef.get(); } @Override public @NotNull Instance instance() { return instance; } @Override public @NotNull Point position() { return position; } @Override public void blockToPlace(@NotNull Block newBlock) { blockToPlaceRef.getAndSet(newBlock); // TODO: Run vanillaHandler.onPlace again on the new block if it's a vanilla block } } @Override public @NotNull Key key() { return Key.key("vri:blocks"); } @Override public @NotNull Set> dependencies() { return Set.of(BlockUpdateFeature.class, DatapackLoadingFeature.class); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/BedBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.EntityPose; import net.minestom.server.entity.Player; import net.minestom.server.entity.metadata.PlayerMeta; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.utils.Direction; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.instance.VanillaExplosion; import org.jetbrains.annotations.NotNull; @SuppressWarnings("UnstableApiUsage") public class BedBlockBehaviour extends VanillaBlockBehaviour { public BedBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } // @Override // protected BlockPropertyList createPropertyValues() { // return new BlockPropertyList().facingProperty("facing").booleanProperty("occupied").property("part", "foot", "head"); // } @Override public void onPlace(@NotNull VanillaPlacement placement) { if (!(placement instanceof VanillaPlacement.HasPlayer hasPlayer)) { return; } Instance instance = placement.instance(); Point pos = placement.position(); Player player = hasPlayer.player(); ItemStack itemStack = player.getItemInMainHand(); // TODO: Hand determination Block bedBlock = itemStack.material().block(); // TODO: Proper block placement management Direction playerDirection = MathUtils.getHorizontalDirection(player.getPosition().yaw()); Point bedHeadPosition = pos.add(playerDirection.normalX(), playerDirection.normalY(), playerDirection.normalZ()); Block blockAtPotentialBedHead = instance.getBlock(bedHeadPosition); if (isReplaceable(blockAtPotentialBedHead)) { Block foot = placeBed(instance, bedBlock, bedHeadPosition, playerDirection); placement.blockToPlace(foot); } else { placement.blockToPlace(placement.instance().getBlock(placement.position())); } } private boolean isReplaceable(Block blockAtPosition) { return blockAtPosition.isAir() || blockAtPosition.isLiquid(); } private Block placeBed(Instance instance, Block bedBlock, Point headPosition, Direction facing) { Block correctFacing = bedBlock.withProperty("facing", facing.name().toLowerCase()); Block footBlock = correctFacing.withProperty("part", "foot"); Block headBlock = correctFacing.withProperty("part", "head").withHandler(new BedBlockBehaviour(this.context)); instance.setBlock(headPosition, headBlock); return footBlock; } @Override public boolean onInteract(@NotNull Interaction interaction) { Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Player player = interaction.getPlayer(); var dimensionKey = instance.getDimensionType(); DimensionType dimension = MinecraftServer.getDimensionTypeRegistry().get(dimensionKey); if (dimension.bedWorks()) { // TODO: make player sleep // TODO: checks for mobs // TODO: check for day // If time is not day // long dayTime = instance.getTime() % 24000L; // if (!(dayTime > 12541L && dayTime < 23458L)) { // return true; // } // Make player sleep PlayerMeta meta = player.getPlayerMeta(); meta.setBedInWhichSleepingPosition(pos); meta.setPose(EntityPose.SLEEPING); // Schedule player getting out of bed MinecraftServer.getSchedulerManager().buildTask(() -> { if (!player.getPlayerConnection().isOnline()) { return; } meta.setBedInWhichSleepingPosition(null); meta.setPose(EntityPose.STANDING); }) .delay(101, TimeUnit.SERVER_TICK) .schedule(); return true; } VanillaExplosion.builder(pos.add(0.5), 5) .isFlaming(true) .build() .apply(instance); return true; } @Override public void onDestroy(@NotNull Destroy destroy) { Instance instance = destroy.getInstance(); Block block = destroy.getBlock(); Point pos = destroy.getBlockPosition(); boolean isHead = "head".equals(block.getProperty("part")); Direction facing = Direction.valueOf(block.getProperty("facing").toUpperCase()); if (isHead) { facing = facing.opposite(); } Point otherPartPosition = pos.add(facing.normalX(), facing.normalY(), facing.normalZ()); instance.setBlock(pos, Block.AIR); instance.setBlock(otherPartPosition, Block.AIR); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/CakeBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; import java.util.Map; import static java.util.Map.entry; public class CakeBlockBehaviour extends VanillaBlockBehaviour { private static final Map candleCakes = Map.ofEntries( entry(Block.CANDLE_CAKE, Material.CANDLE), entry(Block.WHITE_CANDLE_CAKE, Material.WHITE_CANDLE), entry(Block.ORANGE_CANDLE_CAKE, Material.ORANGE_CANDLE), entry(Block.MAGENTA_CANDLE_CAKE, Material.MAGENTA_CANDLE), entry(Block.LIGHT_BLUE_CANDLE_CAKE, Material.LIGHT_BLUE_CANDLE), entry(Block.YELLOW_CANDLE_CAKE, Material.YELLOW_CANDLE), entry(Block.LIME_CANDLE_CAKE, Material.LIME_CANDLE), entry(Block.PINK_CANDLE_CAKE, Material.PINK_CANDLE), entry(Block.GRAY_CANDLE_CAKE, Material.GRAY_CANDLE), entry(Block.LIGHT_GRAY_CANDLE_CAKE, Material.LIGHT_GRAY_CANDLE), entry(Block.CYAN_CANDLE_CAKE, Material.CYAN_CANDLE), entry(Block.PURPLE_CANDLE_CAKE, Material.PURPLE_CANDLE), entry(Block.BLUE_CANDLE_CAKE, Material.BLUE_CANDLE), entry(Block.BROWN_CANDLE_CAKE, Material.BROWN_CANDLE), entry(Block.GREEN_CANDLE_CAKE, Material.GREEN_CANDLE), entry(Block.BLACK_CANDLE_CAKE, Material.BLACK_CANDLE) ); private static final ItemStack flint_and_steel = ItemStack.of(Material.FLINT_AND_STEEL); public CakeBlockBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context); } @Override public boolean onInteract(@NotNull Interaction interaction) { Player player = interaction.getPlayer(); Block block = interaction.getBlock(); Point point = interaction.getBlockPosition(); Instance instance = interaction.getInstance(); int food = player.getFood(); float saturation = player.getFoodSaturation(); ItemStack item = player.getItemInMainHand(); // Player is trying to light candle cake if (item.isSimilar(flint_and_steel) && candleCakes.containsKey(block) && !Boolean.parseBoolean(block.getProperty("lit"))) { instance.setBlock(point, block.withProperty("lit", "true")); // TODO: Handle tool durability return true; } // Player is eating cake if (food < 20) { tryDropCandle(block, instance, point); // Update hunger values int newFood = Math.max(20, food + 2); float newSaturation = Math.max(20f, saturation + 0.4f); player.setFood(newFood); player.setFoodSaturation(newSaturation); } return true; } @Override public void onDestroy(@NotNull Destroy destroy) { Block block = destroy.getBlock(); Instance instance = destroy.getInstance(); Point point = destroy.getBlockPosition(); tryDropCandle(block, instance, point); } private void tryDropCandle(Block block, Instance instance, Point point) { if (block != Block.CAKE) { instance.setBlock(point, Block.CAKE.withProperty("bites", "1")); ItemStack candle = ItemStack.of(candleCakes.get(block)); new ItemEntity(candle).setInstance(instance); } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/ChestBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.kyori.adventure.text.Component; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public class ChestBlockBehaviour extends InventoryBlockBehaviour { public ChestBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context, InventoryType.CHEST_3_ROW, Component.text("Chest")); } @Override public boolean dropContentsOnDestroy() { return true; } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/ConcretePowderBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; // TODO: When placing concrete powder in water, it turns to the solid block correctly, however it falls like a regular concrete powder block public class ConcretePowderBlockBehaviour extends GravityBlockBehaviour { private final Block solidifiedBlock; public ConcretePowderBlockBehaviour(@NotNull VanillaBlocks.BlockContext context, Block solidifiedBlock) { super(context); this.solidifiedBlock = solidifiedBlock; } @Override public void onPlace(@NotNull VanillaBlockBehaviour.VanillaPlacement placement) { super.onPlace(placement); tryConvert(placement.instance(), placement.position()); } @Override public void tick(@NotNull Tick tick) { tryConvert(tick.getInstance(), tick.getBlockPosition()); } private void tryConvert(Instance instance, Point blockPosition) { int x = blockPosition.blockX(); int y = blockPosition.blockY(); int z = blockPosition.blockZ(); // TODO: support block tags if ( instance.getBlock(x, y + 1, z).compare(Block.WATER) || // above instance.getBlock(x - 1, y, z).compare(Block.WATER) || // west instance.getBlock(x + 1, y, z).compare(Block.WATER) || // east instance.getBlock(x, y, z - 1).compare(Block.WATER) || // north instance.getBlock(x, y, z + 1).compare(Block.WATER) // south ) { instance.setBlock(blockPosition, solidifiedBlock); } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/EndPortalBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.dimensions.VanillaDimensionTypes; import org.jetbrains.annotations.NotNull; import java.util.Optional; public class EndPortalBlockBehaviour extends VanillaBlockBehaviour { public EndPortalBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } // @Override // protected BlockPropertyList createPropertyValues() { // return new BlockPropertyList(); // } @Override public void onTouch(@NotNull Touch touch) { Instance instance = touch.getInstance(); Entity touching = touch.getTouching(); var key = instance.getDimensionType(); DimensionType dimension = MinecraftServer.getDimensionTypeRegistry().get(key); DimensionType targetDimension = VanillaDimensionTypes.OVERWORLD; Optional potentialTargetInstance = MinecraftServer.getInstanceManager().getInstances().stream() .filter(in -> { var key1 = in.getDimensionType(); return MinecraftServer.getDimensionTypeRegistry().get(key1) == targetDimension; }) .findFirst(); // TODO: event if (potentialTargetInstance.isPresent()) { Instance targetInstance = potentialTargetInstance.get(); Pos spawnPoint; final int obsidianPlatformX = 100; final int obsidianPlatformY = 48; final int obsidianPlatformZ = 0; if (targetDimension == VanillaDimensionTypes.OVERWORLD) { // teleport to spawn point if (touching instanceof Player) { spawnPoint = ((Player) touching).getRespawnPoint(); } else { // TODO: world spawnpoint spawnPoint = new Pos(0, 80, 0); } } else { // teleport to the obsidian platform, and recreate it if necessary int yLevel = touching instanceof Player ? 49 : 50; spawnPoint = new Pos(obsidianPlatformX, yLevel, obsidianPlatformZ); } if (targetDimension.effects().equals("the_end")) { for (int x = -1; x <= 1; x++) { for (int z = -1; z <= 1; z++) { targetInstance.loadChunk(obsidianPlatformX / 16 + x, obsidianPlatformZ / 16 + z); } } // clear 5x3x5 area around platform for (int x = 0; x < 5; x++) { for (int z = 0; z < 5; z++) { for (int y = 0; y < 3; y++) { targetInstance.setBlock(obsidianPlatformX + x, obsidianPlatformY + y + 1, obsidianPlatformZ + z, Block.AIR); } } } } touching.setInstance(targetInstance); touching.teleport(spawnPoint); } } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/EnderChestBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.system.EnderChestSystem; import org.jetbrains.annotations.NotNull; import java.util.List; public class EnderChestBlockBehaviour extends InventoryBlockBehaviour { public EnderChestBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context, InventoryType.CHEST_3_ROW, Component.text("Ender Chest")); } @Override public boolean dropContentsOnDestroy() { return false; } @Override protected List getAllItems(Instance instance, Point pos, Player player) { return EnderChestSystem.getInstance().getItems(player); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/FireBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.system.NetherPortal; import org.jetbrains.annotations.NotNull; public class FireBlockBehaviour extends VanillaBlockBehaviour { public FireBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } // @Override // protected BlockPropertyList createPropertyValues() { // return new BlockPropertyList().intRange("age", 0, 15); // } @Override public void onTouch(@NotNull Touch touch) { Entity touching = touch.getTouching(); if (!(touching instanceof LivingEntity livingEntity)) { return; } if (livingEntity.isOnFire()) { return; } livingEntity.damage(DamageType.IN_FIRE, 1.0f); livingEntity.setFireTicks(8 * 20); } public void checkForPortal(Instance instance, Point pos, Block block) { NetherPortal portal = NetherPortal.findPortalFrameFromFrameBlock(instance, pos); if (portal == null) { return; } if (portal.tryFillFrame(instance)) { portal.register(instance); } } @Override public void onPlace(@NotNull VanillaPlacement placement) { // check for Nether portal immediately checkForPortal(placement.instance(), placement.position(), placement.blockToPlace()); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/GravityBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaRegistry; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blockupdatesystem.BlockUpdatable; import net.minestom.vanilla.blockupdatesystem.BlockUpdateInfo; import net.minestom.vanilla.blockupdatesystem.BlockUpdateManager; import net.minestom.vanilla.entitymeta.EntityTags; import org.jetbrains.annotations.NotNull; public class GravityBlockBehaviour extends VanillaBlockBehaviour implements BlockUpdatable { public GravityBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } @Override public void onPlace(@NotNull VanillaPlacement placement) { Instance instance = placement.instance(); Point position = placement.position(); Block block = placement.blockToPlace(); if (checkFall(instance, position, block)) { placement.blockToPlace(Block.AIR); } } /** * Checks if a block should fall * * @param instance the instance the block is in * @param position the position of the block * @return true if the block should fall */ public boolean checkFall(Instance instance, Point position, Block block) { Block below = instance.getBlock(position.blockX(), position.blockY() - 1, position.blockZ()); // Exit out now if block below is solid if (below.isSolid()) { return false; } // Schedule block update BlockUpdateManager.from(instance).scheduleNeighborsUpdate(position, BlockUpdateInfo.MOVE_BLOCK()); // Create the context Pos initialPosition = new Pos(position.x() + 0.5f, Math.round(position.y()), position.z() + 0.5f); VanillaRegistry.EntityContext entityContext = context.vri().entityContext(EntityType.FALLING_BLOCK, initialPosition, nbt -> nbt.setTag(EntityTags.FallingBlock.BLOCK, block)); Entity entity = context.vri().createEntityOrDummy(entityContext); // Spawn the entity entity.setInstance(instance, initialPosition); return true; } @Override public void blockUpdate(@NotNull Instance instance, @NotNull Point pos, @NotNull BlockUpdateInfo info) { checkFall(instance, pos, instance.getBlock(pos)); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/InventoryBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.server.tag.Tag; import net.minestom.server.utils.Direction; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blocks.behaviours.chestlike.BlockInventory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnknownNullability; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; /** * Base class for blocks with an inventory. *

* This class needs onPlace to be able to change the block being placed */ public abstract class InventoryBlockBehaviour extends VanillaBlockBehaviour { public static final Tag> TAG_ITEMS = Tag.ItemStack("vri:chest_items").list(); protected static final Random rng = new Random(); protected final InventoryType type; protected final Component title; public InventoryBlockBehaviour(@NotNull VanillaBlocks.BlockContext context, InventoryType type, Component title) { super(context); this.type = type; this.title = title; } @Override public void onPlace(@NotNull VanillaPlacement placement) { Block block = placement.blockToPlace(); Instance instance = placement.instance(); Point pos = placement.position(); @UnknownNullability List items = block.getTag(TAG_ITEMS); if (items != null) { return; } ItemStack[] itemsArray = new ItemStack[type.getSize()]; Arrays.fill(itemsArray, ItemStack.AIR); // Override the block to set Block blockToSet = block.withTag(TAG_ITEMS, List.of(itemsArray)); placement.blockToPlace(blockToSet); } @Override public void onDestroy(@NotNull Destroy destroy) { Instance instance = destroy.getInstance(); Point pos = destroy.getBlockPosition(); Block block = destroy.getBlock(); // TODO: Introduce a way to get the block this is getting replaced with, enabling us to remove the tick delay. destroy.getInstance().scheduleNextTick(ignored -> { if (instance.getBlock(pos).compare(block)) { // Same block, don't remove chest inventory return; } // Different block, remove chest inventory List items = BlockInventory.remove(instance, pos); if (!dropContentsOnDestroy()) { return; } for (ItemStack item : items) { if (item == null) { continue; } ItemEntity entity = new ItemEntity(item); entity.setInstance(destroy.getInstance()); entity.teleport(new Pos(pos.x() + rng.nextDouble(), pos.y() + .5f, pos.z() + rng.nextDouble())); } }); } // @Override // public short getVisualBlockForPlacement(Player player, Player.Hand hand, BlockPosition position) { // // TODO: handle double chests // boolean waterlogged = Block.fromStateId(player.getInstance().getBlockStateId(position.getX(), position.getY(), position.getZ())) == Block.WATER; // float yaw = player.getPosition().getYaw(); // Direction direction = MathUtils.getHorizontalDirection(yaw).opposite(); // return getBaseBlockState().with("facing", direction.name().toLowerCase()).with("waterlogged", String.valueOf(waterlogged)).getBlockId(); // } @Override public boolean onInteract(@NotNull Interaction interaction) { // TODO: handle double chests // TODO: Handle crouching players Block block = interaction.getBlock(); Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Player player = interaction.getPlayer(); Block above = instance.getBlock(pos.blockX(), pos.blockY() + 1, pos.blockZ()); if (above.isSolid()) { // FIXME: chests below transparent blocks cannot be opened return false; } Inventory chestInventory = BlockInventory.from(instance, pos, type, title); player.openInventory(chestInventory); return true; } public abstract boolean dropContentsOnDestroy(); /** * Gets the items in this block only * * @param block the block * @return the items */ protected @NotNull List getItems(Block block) { List items = block.getTag(TAG_ITEMS); if (items == null) { throw new IllegalStateException("Chest block has no items"); } if (items.size() != this.type.getSize()) { throw new IllegalStateException("Invalid items size"); } return items; } /** * Sets the items in this block only * * @param block the block * @param items the items */ protected Block setItems(Block block, List items) { if (items.size() != this.type.getSize()) { throw new IllegalStateException("Invalid items size"); } return block.withTag(TAG_ITEMS, items); } /** * Gets all items represented by this position in this instance * * @param instance the instance * @param pos the position * @return all items in the position in the instance */ protected List getAllItems(Instance instance, Point pos, Player player) { Block block = instance.getBlock(pos); List items = new ArrayList<>(getItems(block)); Point positionOfOtherChest = pos; Direction facing = Direction.valueOf(block.getProperty("facing").toUpperCase()); String type = block.getProperty("type"); switch (type) { case "single" -> { return List.copyOf(items); } case "left" -> positionOfOtherChest = positionOfOtherChest.add(-facing.normalZ(), 0, facing.normalX()); case "right" -> positionOfOtherChest = positionOfOtherChest.add(facing.normalZ(), 0, -facing.normalX()); default -> throw new IllegalArgumentException("Invalid chest type: " + type); } Block otherBlock = instance.getBlock(positionOfOtherChest); BlockHandler handler = otherBlock.handler(); if (handler instanceof InventoryBlockBehaviour chestLike) { items.addAll(chestLike.getItems(otherBlock)); } return List.copyOf(items); } // @Override // public Data readBlockEntity(NBTCompound nbt, Instance instance, BlockPosition position, Data originalData) { // ChestBlockEntity data; // if (originalData instanceof ChestBlockEntity) { // data = (ChestBlockEntity) originalData; // } else { // data = new ChestBlockEntity(position.copy()); // } // // // TODO: CustomName // // TODO: Lock // // TODO: LootTable // // TODO: LootTableSeed // // if (nbt.containsKey("Items")) { // NBTUtils.loadAllItems(nbt.getList("Items"), data.getInventory()); // } // // return data; // } // @Override // public void writeBlockEntity(BlockPosition position, Data blockData, NBTCompound nbt) { // // TODO: CustomName // // TODO: Lock // // TODO: LootTable // // TODO: LootTableSeed // if (blockData instanceof ChestBlockEntity) { // ChestBlockEntity data = (ChestBlockEntity) blockData; // NBTList list = new NBTList<>(NBTTypes.TAG_Compound); // NBTUtils.saveAllItems(list, data.getInventory()); // nbt.set("Items", list); // } // } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/JukeboxBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.MinecraftServer; import net.minestom.server.component.DataComponents; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.jukebox.JukeboxSong; import net.minestom.server.item.ItemStack; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.tag.Tag; import net.minestom.server.worldevent.WorldEvent; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.inventory.InventoryManipulation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.Random; /** * Reimplementation of the jukebox block *

* Requires onPlace enhancements */ public class JukeboxBlockBehaviour extends VanillaBlockBehaviour { public static final Tag DISC_KEY = Tag.ItemStack("minestom:jukebox_disc"); public JukeboxBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } @Override public void onDestroy(@NotNull Destroy destroy) { if (!(destroy instanceof PlayerDestroy)) return; stopPlayback(destroy.getInstance(), destroy.getBlockPosition(), destroy.getBlock()); } public @Nullable ItemStack getDisc(Block block) { return block.getTag(DISC_KEY); } public @NotNull Block withDisc(Block block, @NotNull ItemStack disc) { if (isNotMusicDisc(disc)) { throw new IllegalArgumentException("disc passed to JukeboxBlockHandle#withDisc was not a music disc."); } return block.withTag(DISC_KEY, disc); } private boolean isNotMusicDisc(ItemStack itemStack) { return !itemStack.has(DataComponents.JUKEBOX_PLAYABLE); } @Override public boolean onInteract(@NotNull Interaction interaction) { Player player = interaction.getPlayer(); PlayerHand hand = interaction.getHand(); Instance instance = interaction.getInstance(); Block block = interaction.getBlock(); Point pos = interaction.getBlockPosition(); ItemStack heldItem = player.getItemInMainHand(); ItemStack stack = this.getDisc(block); if (stack != null && !stack.isAir()) { stopPlayback(instance, pos, block); block = block.withTag(DISC_KEY, ItemStack.AIR); instance.setBlock(pos, block.withProperty("has_record", "false")); return true; } if (isNotMusicDisc(heldItem)) { return true; } instance.setBlock(pos, withDisc(block, heldItem).withProperty("has_record", "true")); InventoryManipulation.consumeItemIfNotCreative(player, heldItem, hand); JukeboxSong song = heldItem.get(DataComponents.JUKEBOX_PLAYABLE).holder().resolve(MinecraftServer.getJukeboxSongRegistry()); DynamicRegistry.Key songKey = MinecraftServer.getJukeboxSongRegistry().getKey(song); int songId = MinecraftServer.getJukeboxSongRegistry().getId(songKey); // TODO: Group packet? instance.getPlayers() .stream() .filter(player1 -> player1.getDistance(pos) < 64) .forEach(player1 -> player1.playEffect( WorldEvent.SOUND_PLAY_JUKEBOX_SONG, pos.blockX(), pos.blockY(), pos.blockZ(), songId, false ) ); return true; } @Override public boolean isTickable() { return true; } public void tick(@NotNull Tick tick) { Instance instance = tick.getInstance(); long age = instance.getWorldAge(); // Continue only every 3 seconds if (age % (MinecraftServer.TICK_PER_SECOND * 3L) != 0) { } // TODO: Play sound to all players without the sound playing } // @Override // public Data readBlockEntity(NBTCompound nbt, Instance instance, BlockPosition position, Data originalData) { // JukeboxBlockEntity data; // if (originalData instanceof JukeboxBlockEntity) { // data = (JukeboxBlockEntity) originalData; // } else { // data = new JukeboxBlockEntity(position.copy()); // } // // if(nbt.containsKey("RecordItem")) { // data.setDisc(ItemStack.fromNBT(nbt.getCompound("RecordItem"))); // } // return super.readBlockEntity(nbt, instance, position, originalData); // } // // @Override // public void writeBlockEntity(BlockPosition position, Data blockData, NBTCompound nbt) { // if(blockData instanceof JukeboxBlockEntity) { // JukeboxBlockEntity data = (JukeboxBlockEntity) blockData; // nbt.set("RecordItem", data.getDisc().toNBT()); // } // } /** * Stops playback in an instance */ private void stopPlayback(Instance instance, Point pos, Block block) { ItemEntity discEntity = new ItemEntity(Objects.requireNonNull(getDisc(block))); discEntity.setInstance(instance); discEntity.teleport(new Pos(pos.x() + 0.5f, pos.y() + 1f, pos.z() + 0.5f)); discEntity.setPickable(true); Random rng = new Random(); final float horizontalSpeed = 2f; final float verticalSpeed = 5f; discEntity.setVelocity(new Vec( rng.nextGaussian() * horizontalSpeed, rng.nextFloat() * verticalSpeed, rng.nextGaussian() * horizontalSpeed )); discEntity.setInstance(instance); // TODO: Group Packet? instance.getPlayers().forEach(playerInInstance -> { // stop playback playerInInstance.playEffect( WorldEvent.SOUND_STOP_JUKEBOX_SONG, pos.blockX(), pos.blockY(), pos.blockZ(), -1, false ); }); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/NetherPortalBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.event.Event; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.tag.Tag; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blockupdatesystem.BlockUpdatable; import net.minestom.vanilla.blockupdatesystem.BlockUpdateInfo; import net.minestom.vanilla.dimensions.VanillaDimensionTypes; import net.minestom.vanilla.system.NetherPortal; import net.minestom.vanilla.system.nether.EntityEnterNetherPortalEvent; import net.minestom.vanilla.system.nether.NetherPortalTeleportEvent; import net.minestom.vanilla.system.nether.NetherPortalUpdateEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Optional; public class NetherPortalBlockBehaviour extends VanillaBlockBehaviour implements BlockUpdatable { /** * Time the entity has spent inside a portal. Reset when entering a different portal or by * reentering a portal after leaving one */ public static final Tag TICKS_SPENT_IN_PORTAL_KEY = Tag.Long("minestom:time_spent_in_nether_portal").defaultValue(0L); /** * Prevents multiple updates from different portal blocks */ public static final Tag LAST_PORTAL_UPDATE_KEY = Tag.Long("minestom:last_nether_portal_update_time").defaultValue(Long.MAX_VALUE); /** * Used to check whether the last portal entered is corresponding to this portal block or not */ public static final Tag LAST_PORTAL_KEY = Tag.Long("minestom:last_nether_portal"); /** * Time before teleporting an entity */ public static final Tag PORTAL_COOLDOWN_TIME_KEY = Tag.Long("minestom:nether_portal_cooldown_time").defaultValue(0L); /** * The portal related to this block */ public static final Tag RELATED_PORTAL_KEY = Tag.Long("minestom:related_portal"); public NetherPortalBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } @Override public void onTouch(@NotNull Touch touch) { Block block = touch.getBlock(); Instance instance = touch.getInstance(); Point pos = touch.getBlockPosition(); Entity touching = touch.getTouching(); Long lastPortalUpdate = block.getTag(LAST_PORTAL_UPDATE_KEY); if (lastPortalUpdate == null) { return; } if (lastPortalUpdate < touching.getAliveTicks() - 2) { // if a tick happened with no portal update, that means the entity left the portal at some point Block newBlock = block .withTag(LAST_PORTAL_UPDATE_KEY, 0L) .withTag(TICKS_SPENT_IN_PORTAL_KEY, 0L); instance.setBlock(pos, newBlock); return; } if (lastPortalUpdate == touching.getAliveTicks()) { return; } NetherPortal portal = getPortal(block); long ticksSpentInPortal = updateTimeInPortal(instance, pos, touching, block, portal); Long portalCooldownTime = block.getTag(PORTAL_COOLDOWN_TIME_KEY); if (portalCooldownTime == null) { portalCooldownTime = 0L; } if (ticksSpentInPortal >= portalCooldownTime) { attemptTeleport(instance, touching, block, ticksSpentInPortal, portal); } } private long updateTimeInPortal(Instance instance, Point position, Entity touching, Block block, NetherPortal portal) { Block newBlock = block; newBlock = newBlock.withTag(LAST_PORTAL_UPDATE_KEY, touching.getAliveTicks()); Long ticksSpentInPortal = block.getTag(TICKS_SPENT_IN_PORTAL_KEY); if (ticksSpentInPortal == null) { ticksSpentInPortal = 0L; } NetherPortal portalEntityWasIn = NetherPortal.fromId(newBlock.getTag(LAST_PORTAL_KEY)); if (portal != portalEntityWasIn) { ticksSpentInPortal = 0L; // reset counter } newBlock = newBlock.withTag(LAST_PORTAL_KEY, portal.id()); // data.set(, portal, NetherPortal.class); if (ticksSpentInPortal == 0) { Event event = new EntityEnterNetherPortalEvent(touching, position, portal); MinecraftServer.getGlobalEventHandler().call(event); } ticksSpentInPortal++; newBlock = newBlock.withTag(TICKS_SPENT_IN_PORTAL_KEY, ticksSpentInPortal); instance.setBlock(position, newBlock); Event event = new NetherPortalUpdateEvent(touching, position, portal, instance, ticksSpentInPortal); MinecraftServer.getGlobalEventHandler().call(event); return ticksSpentInPortal; } private void attemptTeleport(Instance instance, Entity touching, Block block, long ticksSpentInPortal, NetherPortal portal) { DimensionType targetDimension; Point position = touching.getPosition(); double targetX = position.x() / 8; double targetY = position.y(); double targetZ = position.z() / 8; var key = instance.getDimensionType(); DimensionType dimension = MinecraftServer.getDimensionTypeRegistry().get(key); if (dimension.effects().equals("nether")) { targetDimension = MinecraftServer.getDimensionTypeRegistry().get(DimensionType.OVERWORLD); targetX = position.x() * 8; targetZ = position.z() * 8; } else { targetDimension = VanillaDimensionTypes.OVERWORLD; } // TODO: event to change portal linking final DimensionType finalTargetDimension = targetDimension; Optional potentialTargetInstance = MinecraftServer.getInstanceManager().getInstances().stream() .filter(in -> { var key1 = in.getDimensionType(); return MinecraftServer.getDimensionTypeRegistry().get(key1) == targetDimension; }) .findFirst(); if (potentialTargetInstance.isEmpty()) { return; } Instance targetInstance = potentialTargetInstance.get(); Pos targetPosition = new Pos(targetX, targetY, targetZ); NetherPortal targetPortal = getCorrespondingNetherPortal(targetInstance, targetPosition); boolean generatePortal = false; if (targetPortal == null) { // no existing portal, will create one NetherPortal.Axis axis = portal.getAxis(); Pos bottomRight = new Pos( targetX - axis.xMultiplier, targetY - 1, targetZ - axis.zMultiplier ); Pos topLeft = new Pos( targetX + 2 * axis.xMultiplier, targetY + 3, targetZ + 2 * axis.zMultiplier ); targetPortal = new NetherPortal(portal.getAxis(), bottomRight, topLeft); generatePortal = true; } targetPosition = calculateTargetPosition(touching, portal, targetPortal); NetherPortalTeleportEvent event = new NetherPortalTeleportEvent(touching, position, portal, ticksSpentInPortal, targetInstance, targetPosition, targetPortal, generatePortal); MinecraftServer.getGlobalEventHandler().call(event); if (!event.isCancelled()) { Block newBlock = block .withTag(LAST_PORTAL_UPDATE_KEY, 0L) .withTag(LAST_PORTAL_KEY, portal.id()) .withTag(TICKS_SPENT_IN_PORTAL_KEY, 0L); instance.setBlock(position, newBlock); teleport(instance, touching, event); } } private @Nullable NetherPortal getCorrespondingNetherPortal(Instance targetInstance, Point targetPosition) { Block block = targetInstance.getBlock(targetPosition); return NetherPortal.fromId(block.getTag(RELATED_PORTAL_KEY)); } private Pos calculateTargetPosition(Entity touching, NetherPortal portal, NetherPortal targetPortal) { Point targetCenter = targetPortal.getCenter(); if (portal == null) { // if this block is not isolated return new Pos( targetCenter.x() + 0.5, targetCenter.y(), targetCenter.z() + 0.5 ); } Pos touchingPos = touching.getPosition(); double touchingX = touchingPos.x(); double touchingY = touchingPos.y(); double touchingZ = touchingPos.z(); Vec portalCenter = portal.getCenter(); double portalCenterX = portalCenter.x(); double portalCenterY = portalCenter.y(); double portalCenterZ = portalCenter.z(); NetherPortal.Axis portalAxis = portal.getAxis(); double portalAxisXMultiplier = portalAxis.xMultiplier; double portalAxisZMultiplier = portalAxis.zMultiplier; NetherPortal.Axis targetAxis = targetPortal.getAxis(); double targetAxisXMultiplier = targetAxis.xMultiplier; double targetAxisZMultiplier = targetAxis.zMultiplier; double relativeX = (touchingX - portalCenterX) / (portal.computeWidth() * portalAxisXMultiplier + portalAxisZMultiplier); double relativeY = (touchingY - portalCenterY) / portal.computeHeight(); double relativeZ = (touchingZ - portalCenterZ) / (portal.computeWidth() * portalAxisZMultiplier + portalAxisXMultiplier); double targetMultiplierX = (targetPortal.computeWidth() * targetAxisXMultiplier + targetAxisZMultiplier); double targetMultiplierY = targetPortal.computeHeight(); double targetMultiplierZ = (targetPortal.computeWidth() * targetAxisZMultiplier + targetAxisXMultiplier); return new Pos( targetCenter.x() + relativeX * targetMultiplierX, targetCenter.y() + relativeY * targetMultiplierY, targetCenter.z() + relativeZ * targetMultiplierZ ); } private void teleport(Instance instance, Entity touching, NetherPortalTeleportEvent event) { Instance targetInstance = event.getTargetInstance(); if (event.createsNewPortal()) { event.getTargetPortal().generate(targetInstance); } if (targetInstance != instance) { touching.setInstance(targetInstance); } Pos targetTeleportationPosition = new Pos(event.getTargetPosition()); touching.teleport(targetTeleportationPosition).thenRun(() -> { Vec velocity = touching.getVelocity(); if ( event.getPortal() != null && event.getPortal().getAxis() != event.getTargetPortal().getAxis() ) { double swapTmp = velocity.x(); touching.setVelocity(new Vec( swapTmp, velocity.z(), swapTmp )); } }); } @Override public void onDestroy(@NotNull Destroy destroy) { Block block = destroy.getBlock(); Instance instance = destroy.getInstance(); NetherPortal netherPortal = getPortal(block); if (netherPortal != null) { netherPortal.breakFrame(instance); netherPortal.unregister(instance); } } private NetherPortal getPortal(Block block) { return NetherPortal.fromId(block.getTag(RELATED_PORTAL_KEY)); } // @Override // public void updateFromNeighbor(Instance instance, Point thisPosition, Point neighborPosition, boolean directNeighbor) { // // } private void breakPortalIfNoLongerValid(Instance instance, Point blockPosition) { NetherPortal netherPortal = getPortal(instance.getBlock(blockPosition)); if (netherPortal == null) { return; } if (netherPortal.isStillValid(instance)) { return; } netherPortal.breakFrame(instance); } public void setRelatedPortal(Instance instance, Point blockPosition, Block block, NetherPortal portal) { instance.setBlock(blockPosition, block.withTag(RELATED_PORTAL_KEY, portal.id())); } @Override public void blockUpdate(@NotNull Instance instance, @NotNull Point pos, @NotNull BlockUpdateInfo info) { breakPortalIfNoLongerValid(instance, pos); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/TNTBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.vanilla.VanillaRegistry; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.entitymeta.EntityTags; import org.jetbrains.annotations.NotNull; import java.util.Random; public class TNTBlockBehaviour extends VanillaBlockBehaviour { public static final Random TNT_RANDOM = new Random(); public TNTBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context); } // @Override // protected BlockPropertyList createPropertyValues() { // return new BlockPropertyList().booleanProperty("unstable"); // } @Override public boolean onInteract(@NotNull Interaction interaction) { Point blockPosition = interaction.getBlockPosition(); Player player = interaction.getPlayer(); PlayerHand hand = interaction.getHand(); if (player.getItemInHand(hand).material() != Material.FLINT_AND_STEEL) { return true; } player.getInstance().setBlock(blockPosition, Block.AIR); spawnPrimedTNT(player.getInstance(), blockPosition, 80); return true; } private void spawnPrimedTNT(Instance instance, Point blockPosition, int fuseTime) { Pos initialPosition = new Pos(blockPosition.blockX() + 0.5f, blockPosition.blockY() + 0f, blockPosition.blockZ() + 0.5f); // Create the entity VanillaRegistry.EntityContext entityContext = context.vri().entityContext(EntityType.TNT, initialPosition, writable -> writable.setTag(EntityTags.PrimedTnt.FUSE_TIME, fuseTime)); Entity primedTnt = context.vri().createEntityOrDummy(entityContext); // Spawn it with random velocity primedTnt.setInstance(instance, initialPosition); primedTnt.setVelocity(new Vec(TNT_RANDOM.nextFloat() * 2f - 1f, TNT_RANDOM.nextFloat() * 5f, TNT_RANDOM.nextFloat() * 2f - 1f)); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/TrappedChestBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours; import net.kyori.adventure.text.Component; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public class TrappedChestBlockBehaviour extends InventoryBlockBehaviour { // TODO: redstone signal public TrappedChestBlockBehaviour(@NotNull VanillaBlocks.BlockContext context) { super(context, InventoryType.CHEST_3_ROW, Component.text("Trapped Chest")); } // @Override // protected BlockPropertyList createPropertyValues() { // return super.createPropertyValues().property("type", "single", "left", "right"); // } @Override public boolean dropContentsOnDestroy() { return true; } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/chestlike/BlockInventory.java ================================================ package net.minestom.vanilla.blocks.behaviours.chestlike; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.blocks.behaviours.InventoryBlockBehaviour; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class BlockInventory extends Inventory { private static final Map BLOCK_INVENTORY_MAP = new ConcurrentHashMap<>(); protected final Instance instance; protected final Point pos; private BlockInventory(Instance instance, Point pos, InventoryType inventoryType, Component title) { super(inventoryType, title); this.instance = instance; this.pos = pos; // Set items List itemsList = instance.getBlock(pos).getTag(InventoryBlockBehaviour.TAG_ITEMS); if (itemsList != null) { for (int i = 0; i < itemsList.size(); i++) { this.itemStacks[i] = itemsList.get(i); } } } public static BlockInventory from(Instance instance, Point pos, InventoryType inventoryType, Component title) { BlockInventory inv = BLOCK_INVENTORY_MAP.get(pos); if (inv == null) { inv = new BlockInventory(instance, pos, inventoryType, title); BLOCK_INVENTORY_MAP.put(pos, inv); } if (inv.getInventoryType() != inventoryType) { throw new IllegalStateException("Inventory type mismatch"); } if (!inv.getTitle().equals(title)) { throw new IllegalStateException("Inventory title mismatch"); } return inv; } public static @NotNull List remove(Instance instance, Point pos) { BlockInventory inv = BLOCK_INVENTORY_MAP.get(pos); if (inv == null) { return List.of(); } BLOCK_INVENTORY_MAP.remove(pos); return List.of(inv.itemStacks); } @Override public void setItemStack(int slot, @NotNull ItemStack itemStack) { super.setItemStack(slot, itemStack); instance.setBlock(pos, instance.getBlock(pos).withTag(InventoryBlockBehaviour.TAG_ITEMS, List.of(itemStacks))); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/chestlike/BlockItems.java ================================================ package net.minestom.vanilla.blocks.behaviours.chestlike; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.tag.Tag; import net.minestom.vanilla.tag.Tags; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; import java.util.*; import java.util.stream.Collectors; /** * The key difference between BlockItems and BlockInventory is that BlockItems * does not require specifying an inventoryType or title. It is designed for simpler * container block management. */ public class BlockItems { private static final Tag> FALLBACK_TAG_ITEMS = Tag.ItemStack("vri:container_items") .list(); private static Map>> TAG_ITEMS_BY_BLOCK; private static Tag> getTagForBlock(Block block) { return TAG_ITEMS_BY_BLOCK.getOrDefault(block.name(), FALLBACK_TAG_ITEMS).defaultValue(List.of()); } private final List items; private BlockItems(List items) { this.items = new ArrayList<>(items); } public static BlockItems from(Block block) { return new BlockItems(block.getTag(getTagForBlock(block))); } public static BlockItems from(Block block, int requireStacks) { BlockItems items = from(block); if (items.size() != requireStacks) { items.requireStacks(requireStacks); } return items; } private void requireStacks(int requireStacks) { // remove from the top, or add to the top. if (items.size() < requireStacks) { items.addAll(Collections.nCopies(requireStacks - items.size(), ItemStack.AIR)); } else if (items.size() > requireStacks) { items.subList(requireStacks, items.size()).clear(); } } public @UnmodifiableView @NotNull List itemStacks() { return Collections.unmodifiableList(items); } public int size() { return items.size(); } public boolean isEmpty() { return items.isEmpty(); } public boolean isAir() { return items.isEmpty() || items.stream().allMatch(ItemStack::isAir); } public Block apply(Block block) { return block.withTag(getTagForBlock(block), items); } public void setItems(List itemStacks) { items.clear(); items.addAll(itemStacks); } public ItemStack get(int index) { return items.get(index); } public void set(int index, ItemStack item) { items.set(index, item); } static { Map>> tagItemsByBlock = new HashMap<>(); // some blocks need to be represented by a specific nbt tag to be visible to the client. tagItemsByBlock.put(Block.CAMPFIRE, Tags.Blocks.Campfire.ITEMS); BlockItems.TAG_ITEMS_BY_BLOCK = Map.copyOf(tagItemsByBlock.entrySet().stream() .map(entry -> Map.entry(entry.getKey().name(), entry.getValue())) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue))); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/chestlike/DoubleChestInventory.java ================================================ package net.minestom.vanilla.blocks.behaviours.chestlike; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.List; import java.util.stream.Stream; public class DoubleChestInventory extends Inventory { private final BlockInventory left; private final BlockInventory right; public DoubleChestInventory(BlockInventory left, BlockInventory right, String title) { super(InventoryType.CHEST_6_ROW, title); this.left = left; this.right = right; } @Override public @NotNull ItemStack getItemStack(int slot) { if (slot < left.getSize()) { return left.getItemStack(slot); } return right.getItemStack(slot - left.getSize()); } @Override public void setItemStack(int slot, @NotNull ItemStack itemStack) { if (slot < left.getSize()) { left.setItemStack(slot, itemStack); } else { right.setItemStack(slot - left.getSize(), itemStack); } } @Override @Deprecated public ItemStack[] getItemStacks() { return itemStacks().toArray(ItemStack[]::new); } public List itemStacks() { return Stream.of(left, right) .map(BlockInventory::getItemStacks) .map(List::of) .flatMap(Collection::stream) .toList(); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/oxidisable/OxidatableBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.oxidisable; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.inventory.InventoryManipulation; import net.minestom.vanilla.randomticksystem.RandomTickable; import net.minestom.vanilla.utils.MathUtils; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Objects; import java.util.Random; /** * Oxidation (Source) *

* Non-waxed copper blocks have four stages of oxidation (including the initial normal state). Lightning bolts and * axes can remove the oxidation on copper blocks. As the block begins to oxidize (exposed copper), it gets * discolored and green spots begin to appear. As the oxidation continues (weathered copper), the block is a green * color with brown spots. In the last stage (oxidized copper), the block is teal with several green spots. * Oxidation of copper blocks relies only on random ticks. Rain or water does not accelerate oxidation, and covering * copper blocks with other blocks does not prevent oxidation. In Java Edition, groups of non-waxed copper blocks * oxidize far more slowly than single copper blocks that are spaced at least 4 blocks apart. This is because a * block in a group being less oxidized than the others slows down the oxidation process for all other blocks within * 4 blocks of taxicab distance. However, if one wishes to increase the oxidation speed, placing oxidized copper * blocks around less oxidized copper blocks does not offer a speed improvement over simply placing the blocks 4 * apart. The calculations for the oxidation behavior are as follows: * In Java Edition, when a random tick is given, a copper block has a 64/1125 chance to enter a state called * pre-oxidation. This means a copper block enters pre-oxidation after approximately 20 minutes. * In pre-oxidation, the copper block searches its nearby non-waxed copper blocks for a distance of 4 blocks taxicab * distance. If there is any copper block that has a lower oxidation level, then the pre-oxidation ends, meaning * that this copper block does not weather. Let a be the number of all nearby non-waxed copper blocks, and b be the * number of nearby non-waxed copper blocks that have a higher oxidation level. We derive the value of c from this * equation: c = b + 1/a + 1. We also let the modifying factor m be 0.75 if the copper block has no oxidation level, * or 1 if the copper block is exposed or weathered.[1] Then the oxidation probability is mc2. For example, an * unweathered copper block surrounded by 6 unweathered copper blocks and 6 exposed copper blocks has a 21.7% chance * to oxidize if it enters the pre-oxidation state. In this case, a = 12, b = 6, and m = 0.75.[2] The most efficient * way of laying out the copper blocks for oxidation is in a 7×7×6 face-centered cubic (fcc)/cubic close-packed * (ccp) lattice. *

*/ public class OxidatableBlockBehaviour extends WaxableBlockBehaviour implements RandomTickable, OxygenSensitive { private final short previous; private final short oxidised; private final int oxidisedLevel; public OxidatableBlockBehaviour(VanillaBlocks.@NotNull BlockContext context, Block previous, Block oxidised, Block waxed, int oxidisedLevel) { super(context, waxed); this.previous = (short) previous.stateId(); this.oxidised = (short) oxidised.stateId(); this.oxidisedLevel = oxidisedLevel; } @Override public void randomTick(@NotNull RandomTick randomTick) { // Exit now if the block cannot be oxidised anymore if (oxidised == context.stateId()) return; Random random = context.vri().random(randomTick.instance()); // In Java Edition, when a random tick is given, a copper block has a 64/1125 chance to enter a state called pre-oxidation. // This means a copper block enters pre-oxidation after approximately 20 minutes. if (random.nextInt(1125) >= 64) { return; } // In pre-oxidation, the copper block searches its nearby non-waxed copper blocks for a distance of 4 blocks // taxicab distance. If there is any copper block that has a lower oxidation level, then the pre-oxidation ends, // meaning that this copper block does not weather. List nearbyBlocks = MathUtils.getWithinManhattanDistance(randomTick.position(), 4) .stream() .map(point -> randomTick.instance().isChunkLoaded(point) ? randomTick.instance().getBlock(point) : Block.AIR) .toList(); int minOxidisedAround = nearbyBlocks.stream() .filter(block -> block.handler() instanceof OxygenSensitive) .map(block -> (OxygenSensitive) block.handler()) .mapToInt(OxygenSensitive::oxidisedLevel) .min() .orElse(Integer.MAX_VALUE); if (minOxidisedAround < oxidisedLevel) { return; } // Let a be the number of all nearby non-waxed copper blocks, and b be the number of nearby non-waxed copper // blocks that have a higher oxidation level. We derive the value of c from this equation: c = b + 1/a + 1. // We also let the modifying factor m be 0.75 if the copper block has no oxidation level, or 1 if the copper // block is exposed or weathered.[1] Then the oxidation probability is mc2. // For example, an unweathered copper block surrounded by 6 unweathered copper blocks and 6 exposed copper // blocks has a 21.7% chance to oxidize if it enters the pre-oxidation state. In this case, a = 12, b = 6, and // m = 0.75.[2] double a = (int) nearbyBlocks.stream() .filter(block -> block.handler() instanceof OxygenSensitive os && os.oxidisedLevel() == oxidisedLevel()) // Filter out unrelated blocks .count(); double b = (int) nearbyBlocks.stream() .filter(block -> !(block.handler() instanceof WaxedBlockBehaviour)) .filter(block -> block.handler() instanceof OxygenSensitive os && os.oxidisedLevel() > oxidisedLevel()) // Filter out unrelated blocks .map(block -> (OxygenSensitive) block.handler()).filter(Objects::nonNull) .filter(handler -> handler.oxidisedLevel() > oxidisedLevel) .count(); double m = oxidisedLevel == 0 ? 0.75 : 1; double c = (b + 1) / (a + 1); double probability = m * c * c; if (random.nextDouble() < probability) { Block block = Block.fromStateId(oxidised); Objects.requireNonNull(block, "Block with state id " + oxidised + " was not found"); randomTick.instance().setBlock(randomTick.position(), block); } } @Override public int oxidisedLevel() { return oxidisedLevel; } @Override public boolean onInteract(@NotNull Interaction interaction) { PlayerHand hand = interaction.getHand(); Player player = interaction.getPlayer(); Block interactionBlock = interaction.getBlock(); ItemStack item = player.getItemInHand(hand); Material material = item.material(); if (interactionBlock.stateId() != previous && material.key().value().toLowerCase().contains("_axe")) { // TODO: Better way to check if it's an axe Block previousBlock = Block.fromStateId(previous); Objects.requireNonNull(previousBlock, "Block with state id " + previous + " was not found"); interaction.getInstance().setBlock(interaction.getBlockPosition(), previousBlock); InventoryManipulation.damageItemIfNotCreative(player, hand, 1); return false; } return super.onInteract(interaction); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/oxidisable/OxidatedBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.oxidisable; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public abstract class OxidatedBlockBehaviour extends WaxableBlockBehaviour implements OxygenSensitive { private final int oxidisedLevel; public OxidatedBlockBehaviour(VanillaBlocks.@NotNull BlockContext context, Block waxed, int oxidisedLevel) { super(context, waxed); this.oxidisedLevel = oxidisedLevel; } @Override public int oxidisedLevel() { return oxidisedLevel; } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/oxidisable/OxygenSensitive.java ================================================ package net.minestom.vanilla.blocks.behaviours.oxidisable; public interface OxygenSensitive { int oxidisedLevel(); } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/oxidisable/WaxableBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.oxidisable; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.inventory.InventoryManipulation; import org.jetbrains.annotations.NotNull; import java.util.Objects; public abstract class WaxableBlockBehaviour extends VanillaBlockBehaviour { protected final short waxedBlock; protected WaxableBlockBehaviour(VanillaBlocks.@NotNull BlockContext context, Block waxedTarget) { super(context); this.waxedBlock = (short) waxedTarget.stateId(); } @Override public boolean onInteract(@NotNull Interaction interaction) { PlayerHand hand = interaction.getHand(); Player player = interaction.getPlayer(); ItemStack item = player.getItemInHand(hand); Material material = item.material(); if (Material.HONEYCOMB.equals(material)) { Block block = Block.fromStateId(waxedBlock); Objects.requireNonNull(block, "Waxed block with state id " + waxedBlock + " does not exist"); interaction.getInstance().setBlock(interaction.getBlockPosition(), block); InventoryManipulation.consumeItemIfNotCreative(player, hand, 1); return false; } return super.onInteract(interaction); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/oxidisable/WaxedBlockBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.oxidisable; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.inventory.InventoryManipulation; import org.jetbrains.annotations.NotNull; import java.util.Objects; public class WaxedBlockBehaviour extends OxidatedBlockBehaviour { private final short unWaxed; public WaxedBlockBehaviour(VanillaBlocks.@NotNull BlockContext context, Block unWaxed, int oxidisedLevel) { super(context, Block.fromStateId(context.stateId()), oxidisedLevel); this.unWaxed = (short) unWaxed.stateId(); } @Override public boolean onInteract(@NotNull Interaction interaction) { PlayerHand hand = interaction.getHand(); Player player = interaction.getPlayer(); ItemStack item = player.getItemInHand(hand); Material material = item.material(); if (material.key().value().toLowerCase().contains("_axe")) { // TODO: Better way to check if it's an axe Block previousBlock = Block.fromStateId(unWaxed); Objects.requireNonNull(previousBlock, "Previous block with state id " + unWaxed + " was not found"); interaction.getInstance().setBlock(interaction.getBlockPosition(), previousBlock); InventoryManipulation.damageItemIfNotCreative(player, hand, 1); return false; } return super.onInteract(interaction); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/BlastingFurnaceBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blocks.behaviours.InventoryBlockBehaviour; import net.minestom.vanilla.blocks.behaviours.chestlike.BlockInventory; import net.minestom.vanilla.events.BlastingFurnaceTickEvent; import org.jetbrains.annotations.NotNull; public class BlastingFurnaceBehaviour extends InventoryBlockBehaviour { public BlastingFurnaceBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context, InventoryType.BLAST_FURNACE, Component.text("Blast Furnace")); } @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.BLAST_FURNACE, Component.text("Blast Furnace")); Player player = interaction.getPlayer(); player.openInventory(inventory); return false; } @Override public boolean dropContentsOnDestroy() { return true; } @Override public boolean isTickable() { return true; } @Override public void tick(@NotNull BlockHandler.Tick tick) { var events = this.context.vri().process().eventHandler(); if (!events.hasListener(BlastingFurnaceTickEvent.class)) return; // fast exit since this is hot code Instance instance = tick.getInstance(); Point pos = tick.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.BLAST_FURNACE, Component.text("Blast Furnace")); BlastingFurnaceTickEvent event = new BlastingFurnaceTickEvent(tick.getBlock(), tick.getInstance(), new BlockVec(tick.getBlockPosition()), inventory); events.call(event); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/CampfireBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.recipe.*; import net.minestom.server.recipe.display.RecipeDisplay; import net.minestom.server.recipe.display.SlotDisplay; import net.minestom.server.tag.Tag; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blocks.behaviours.chestlike.BlockItems; import net.minestom.vanilla.inventory.InventoryManipulation; import net.minestom.vanilla.tag.Tags.Blocks.Campfire; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.stream.IntStream; public class CampfireBehaviour extends VanillaBlockBehaviour { private static final int CONTAINER_SIZE = 4; private static final Random RNG = new Random(); private final RecipeManager recipeManager; public CampfireBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context); this.recipeManager = context.vri().process().recipe(); } public BlockItems getBlockItems(Block block) { return BlockItems.from(block, CONTAINER_SIZE); } public @NotNull Block withCookingProgress(Block block, int slotIndex, int cookingTime) { List cookingProgress = new ArrayList<>(block.getTag(Campfire.COOKING_PROGRESS)); cookingProgress.set(slotIndex, cookingTime); return block.withTag(Campfire.COOKING_PROGRESS, cookingProgress); } /** * Appends an id to the first available slot in the campfire. * @return the index of the slot the id was appended to. */ public int appendItem(BlockItems items, @NotNull Material material) { OptionalInt freeSlot = findFirstFreeSlot(items.itemStacks()); if (freeSlot.isEmpty()) throw new IllegalArgumentException("Campfire doesn't have free slot for appending an id in CampfireBehaviour#appendItem"); ItemStack ingredient = ItemStack.of(material); int index = freeSlot.getAsInt(); items.set(index, ingredient); return index; } @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Block block = interaction.getBlock(); Player player = interaction.getPlayer(); ItemStack input = player.getItemInHand(interaction.getHand()); Optional recipeOptional = findCampfireCookingRecipe(input); if (recipeOptional.isEmpty()) return true; BlockItems items = getBlockItems(block); if (findFirstFreeSlot(items.itemStacks()).isEmpty()) return true; boolean itemNotConsumed = !InventoryManipulation.consumeItemIfNotCreative(player, interaction.getHand(), 1); if (itemNotConsumed) return true; Recipe recipe = recipeOptional.get(); if (!(recipe.createRecipeDisplays().getFirst() instanceof RecipeDisplay.Furnace furnaceRecipe)) return false; Material material = getMaterialFromSlotDisplay(furnaceRecipe.ingredient()); if (material == null) return false; int index = appendItem(items, material); block = withCookingProgress(block, index, furnaceRecipe.duration()); instance.setBlock(pos, items.apply(block)); return false; } @Override public void onDestroy(@NotNull BlockHandler.Destroy destroy) { Instance instance = destroy.getInstance(); Point pos = destroy.getBlockPosition(); Block block = destroy.getBlock(); // TODO: Introduce a way to get the block this is getting replaced with, enabling us to remove the tick delay. instance.scheduleNextTick(ignored -> { Block newBlock = instance.getBlock(pos); if (newBlock.compare(block)) { // Same block, don't remove campfire return; } // Different block, remove campfire List items = BlockItems.from(newBlock).itemStacks(); for (ItemStack item : items) { if (item == null) { continue; } dropItem(instance, pos, item); } }); } @Override public void tick(@NotNull BlockHandler.Tick tick) { Instance instance = tick.getInstance(); Block block = tick.getBlock(); Point pos = tick.getBlockPosition(); BlockItems items = getBlockItems(block); if (items.isAir()) return; List cookingProgress = new ArrayList<>(block.getTag(Campfire.COOKING_PROGRESS)); boolean lit = Boolean.parseBoolean(block.getProperty("lit")); if (!lit) { for (ItemStack item : items.itemStacks()) dropItem(instance, pos, item); items.setItems(Collections.nCopies(4, ItemStack.AIR)); instance.setBlock(pos, items.apply(block)); return; } for (ListIterator i = cookingProgress.listIterator(); i.hasNext(); ) { int index = i.nextIndex(); Integer progress = i.next(); Material inputMaterial = items.get(index).material(); if (Material.AIR.equals(inputMaterial)) continue; if (progress <= 0) { endCampfireCookingProgress(tick.getInstance(), tick.getBlockPosition(), ItemStack.of(inputMaterial)); items.set(index, ItemStack.AIR); continue; } progress -= 1; i.set(progress); } block = items.apply(block); block = block.withTag(Campfire.COOKING_PROGRESS, cookingProgress); instance.setBlock(pos, block); } @Override public @NotNull Collection> getBlockEntityTags() { return List.of(Campfire.ITEMS); } @Override public boolean isTickable() { return true; } private void endCampfireCookingProgress(Instance instance, Point pos, ItemStack input) { Optional recipeOptional = findCampfireCookingRecipe(input); if (recipeOptional.isEmpty()) throw new IllegalArgumentException("Cannot end campfire cooking progress because input recipe doesn't found"); Material material = getRecipeResult(recipeOptional.get()); if (material == null) { return; } dropItem(instance, pos, ItemStack.of(material)); } private void dropItem(Instance instance, Point pos, ItemStack item) { ItemEntity resultItemEntity = new ItemEntity(item); resultItemEntity.setInstance(instance); resultItemEntity.teleport(new Pos(pos.x() + RNG.nextDouble(), pos.y() + .5f, pos.z() + RNG.nextDouble())); } private OptionalInt findFirstFreeSlot(List items) { return IntStream.range(0, items.size()).filter(index -> items.get(index).isAir()).findFirst(); } private Material getRecipeResult(Recipe recipe) { RecipeDisplay d = recipe.createRecipeDisplays().getFirst(); if (d instanceof RecipeDisplay.Furnace f) { return getMaterialFromSlotDisplay(f.result()); } return null; } private Material getRecipeInput(Recipe recipe) { RecipeDisplay d = recipe.createRecipeDisplays().getFirst(); if (d instanceof RecipeDisplay.Furnace f) { return getMaterialFromSlotDisplay(f.ingredient()); } return null; } private Material getMaterialFromSlotDisplay(SlotDisplay slotDisplay) { return switch (slotDisplay) { case SlotDisplay.Item i -> i.material(); case SlotDisplay.ItemStack i -> i.itemStack().material(); default -> null; }; } private Optional findCampfireCookingRecipe(ItemStack input) { if (input == null) return Optional.empty(); return recipeManager .getRecipes().stream() .filter(r -> r.recipeBookCategory() == RecipeBookCategory.CAMPFIRE) .filter(recipe -> input.material().equals(getRecipeInput(recipe))).findFirst(); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/CraftingTableBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.minestom.server.entity.Player; import net.minestom.server.event.EventListener; import net.minestom.server.event.inventory.InventoryCloseEvent; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public class CraftingTableBehaviour extends VanillaBlockBehaviour { public CraftingTableBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context); } @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Inventory inventory = new Inventory(InventoryType.CRAFTING, "Crafting Table"); Player player = interaction.getPlayer(); player.openInventory(inventory); player.eventNode().addListener( EventListener.builder(InventoryCloseEvent.class) .filter(event -> inventory == event.getInventory()) .expireCount(1) .handler(event -> { // TODO: Drop all items instead of adding them to the player's inventory? for (ItemStack itemStack : inventory.getItemStacks()) { player.getInventory().addItemStack(itemStack); } }) .build() ); return false; } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/FurnaceBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blocks.behaviours.InventoryBlockBehaviour; import net.minestom.vanilla.blocks.behaviours.chestlike.BlockInventory; import net.minestom.vanilla.events.FurnaceTickEvent; import org.jetbrains.annotations.NotNull; public class FurnaceBehaviour extends InventoryBlockBehaviour { public FurnaceBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context, InventoryType.FURNACE, Component.text("Furnace")); } @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.FURNACE, Component.text("Furnace")); Player player = interaction.getPlayer(); player.openInventory(inventory); return false; } @Override public boolean dropContentsOnDestroy() { return true; } @Override public boolean isTickable() { return true; } @Override public void tick(@NotNull BlockHandler.Tick tick) { var events = this.context.vri().process().eventHandler(); if (!events.hasListener(FurnaceTickEvent.class)) return; // fast exit since this is hot code Instance instance = tick.getInstance(); Point pos = tick.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.FURNACE, Component.text("Furnace")); FurnaceTickEvent event = new FurnaceTickEvent(tick.getBlock(), tick.getInstance(), new BlockVec(tick.getBlockPosition()), inventory); events.call(event); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/SmithingTableBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.minestom.server.entity.Player; import net.minestom.server.event.EventListener; import net.minestom.server.event.inventory.InventoryCloseEvent; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public class SmithingTableBehaviour extends VanillaBlockBehaviour { public SmithingTableBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context); } // TODO: block placement facing @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Inventory inventory = new Inventory(InventoryType.SMITHING, "Upgrade Gear"); Player player = interaction.getPlayer(); player.openInventory(inventory); player.eventNode().addListener( EventListener.builder(InventoryCloseEvent.class) .filter(event -> inventory == event.getInventory()) .expireCount(1) .handler(event -> { // TODO: Drop all items instead of adding them to the player's inventory? for (int i = 0; i < 3; i++) { player.getInventory().addItemStack(inventory.getItemStack(i)); } }) .build() ); return false; } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/SmokerBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlocks; import net.minestom.vanilla.blocks.behaviours.InventoryBlockBehaviour; import net.minestom.vanilla.blocks.behaviours.chestlike.BlockInventory; import net.minestom.vanilla.events.SmokerTickEvent; import org.jetbrains.annotations.NotNull; public class SmokerBehaviour extends InventoryBlockBehaviour { public SmokerBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context, InventoryType.SMOKER, Component.text("Smoker")); } @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Instance instance = interaction.getInstance(); Point pos = interaction.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.SMOKER, Component.text("Smoker")); Player player = interaction.getPlayer(); player.openInventory(inventory); return false; } @Override public boolean dropContentsOnDestroy() { return true; } @Override public boolean isTickable() { return true; } @Override public void tick(@NotNull BlockHandler.Tick tick) { var events = this.context.vri().process().eventHandler(); if (!events.hasListener(SmokerTickEvent.class)) return; // fast exit since this is hot code Instance instance = tick.getInstance(); Point pos = tick.getBlockPosition(); Inventory inventory = BlockInventory.from(instance, pos, InventoryType.SMOKER, Component.text("Smoker")); SmokerTickEvent event = new SmokerTickEvent(tick.getBlock(), tick.getInstance(), new BlockVec(tick.getBlockPosition()), inventory); events.call(event); } } ================================================ FILE: blocks/src/main/java/net/minestom/vanilla/blocks/behaviours/recipe/StonecutterBehaviour.java ================================================ package net.minestom.vanilla.blocks.behaviours.recipe; import net.minestom.server.entity.Player; import net.minestom.server.event.EventListener; import net.minestom.server.event.inventory.InventoryCloseEvent; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.blocks.VanillaBlockBehaviour; import net.minestom.vanilla.blocks.VanillaBlocks; import org.jetbrains.annotations.NotNull; public class StonecutterBehaviour extends VanillaBlockBehaviour { public StonecutterBehaviour(VanillaBlocks.@NotNull BlockContext context) { super(context); } // TODO: block placement facing @Override public boolean onInteract(@NotNull BlockHandler.Interaction interaction) { Inventory inventory = new Inventory(InventoryType.STONE_CUTTER, "Stonecutter"); Player player = interaction.getPlayer(); player.openInventory(inventory); player.eventNode().addListener( EventListener.builder(InventoryCloseEvent.class) .filter(event -> inventory == event.getInventory()) .expireCount(1) .handler(event -> { // TODO: Drop all items instead of adding them to the player's inventory? player.getInventory().addItemStack(inventory.getItemStack(0)); }) .build() ); return false; } } ================================================ FILE: build.gradle.kts ================================================ plugins { java `java-library` `maven-publish` id("com.github.harbby.gradle.serviceloader") version ("1.1.8") id("io.github.goooler.shadow") version ("8.1.8") } subprojects { plugins.apply("java") plugins.apply("java-library") plugins.apply("maven-publish") plugins.apply("com.github.harbby.gradle.serviceloader") plugins.apply("io.github.goooler.shadow") group = "net.minestom.vanilla" version = "indev" java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 // withJavadocJar() withSourcesJar() sourceSets.main { java.srcDir("src/main/java") } } tasks.withType { duplicatesStrategy = DuplicatesStrategy.INCLUDE } tasks.withType { gradleVersion = rootProject.gradle.gradleVersion } repositories { mavenCentral() maven(url = "https://jitpack.io") mavenLocal() } dependencies { } publishing { publications { register("maven", MavenPublication::class) { from(components["java"]) } } } serviceLoader.serviceInterfaces.add("net.minestom.vanilla.VanillaReimplementation\$Feature") serviceLoader.serviceInterfaces.add("org.slf4j.spi.SLF4JServiceProvider") tasks.getByName("build").dependsOn("shadowJar") tasks.withType { dependsOn("serviceLoaderBuild") useJUnitPlatform() } } ================================================ FILE: commands/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":instance-meta")) } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/DifficultyCommand.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.exception.ArgumentSyntaxException; import net.minestom.server.world.Difficulty; import org.jetbrains.annotations.NotNull; /** * Command that make an instance change difficulty */ public class DifficultyCommand extends Command { public DifficultyCommand() { super("difficulty"); setCondition(this::isAllowed); setDefaultExecutor(this::usage); Argument difficulty = ArgumentType.Word("difficulty").from("peaceful", "easy", "normal", "hard"); difficulty.setCallback(this::difficultyCallback); addSyntax(this::execute, difficulty); } private void usage(CommandSender player, CommandContext arguments) { player.sendMessage("Usage: /difficulty (peaceful|easy|normal|hard)"); } private void execute(CommandSender player, CommandContext arguments) { String difficultyName = arguments.get("difficulty"); Difficulty difficulty = Difficulty.valueOf(difficultyName.toUpperCase()); MinecraftServer.setDifficulty(difficulty); player.sendMessage("You are now playing in " + difficultyName); } private void difficultyCallback(@NotNull CommandSender sender, @NotNull ArgumentSyntaxException exception) { sender.sendMessage("'" + exception.getInput() + "' is not a valid difficulty!"); } private boolean isAllowed(CommandSender player, String commandName) { return true; // TODO: permissions } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/ForceloadCommand.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.coordinate.CoordConversion; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; import net.minestom.server.instance.Instance; import net.minestom.server.utils.location.RelativeVec; import net.minestom.vanilla.instancemeta.tickets.TicketManager; import net.minestom.vanilla.instancemeta.tickets.TicketUtils; import java.util.List; /** * "forceload": * Description: "Forces chunks to constantly be loaded or not. " * BE: false * EE: false * JE: true * OP_Level: 2 * BE_EE_OP_Level: 0 * MP_Only: false */ public class ForceloadCommand extends Command { public ForceloadCommand() { super("forceload"); // forceload add [] // Forces the chunk at the position (through to if set) in the dimension of the command's execution to be loaded constantly. this.addSyntax( this::usageAddFrom, ArgumentType.Literal("add"), ArgumentType.RelativeVec2("from") ); this.addSyntax( this::usageAddFromTo, ArgumentType.Literal("add"), ArgumentType.RelativeVec2("from"), ArgumentType.RelativeVec2("to") ); // forceload remove [] // Unforces the chunk at the position (through to if set) in the dimension of the command's execution to be loaded constantly. this.addSyntax( this::usageRemoveFrom, ArgumentType.Literal("remove"), ArgumentType.RelativeVec2("from") ); this.addSyntax( this::usageRemoveFromTo, ArgumentType.Literal("remove"), ArgumentType.RelativeVec2("from"), ArgumentType.RelativeVec2("to") ); } private void addForceLoad(Instance instance, int chunkX, int chunkZ) { addForceLoad(instance, CoordConversion.chunkIndex(chunkX, chunkZ)); } private void addForceLoad(Instance instance, long chunkIndex) { TicketManager.Ticket ticketToAdd = TicketManager.Ticket.from(TicketManager.FORCED_TICKET, chunkIndex); TicketUtils.waitingTickets(instance, List.of(ticketToAdd)); } private void removeForceLoad(Instance instance, int chunkX, int chunkZ) { removeForceLoad(instance, CoordConversion.chunkIndex(chunkX, chunkZ)); } private void removeForceLoad(Instance instance, long chunkIndex) { TicketManager.Ticket ticketToRemove = TicketManager.Ticket.from(TicketManager.FORCED_TICKET, chunkIndex); TicketUtils.removingTickets(instance, List.of(ticketToRemove)); } private void usageAddFrom(CommandSender sender, CommandContext context) { if (!(sender instanceof Player player)) { sender.sendMessage("This command must be executed by a player!"); return; } RelativeVec fromVec = context.get("from"); Vec position = fromVec.from(player.getPosition()); // Get chunk position int chunkX = CoordConversion.globalToChunk(position.x()); int chunkZ = CoordConversion.globalToChunk(position.z()); // Add the force load Instance instance = player.getInstance(); addForceLoad(instance, chunkX, chunkZ); } private void usageAddFromTo(CommandSender sender, CommandContext context) { if (!(sender instanceof Player player)) { sender.sendMessage("This command must be executed by a player!"); return; } RelativeVec fromVec = context.get("from"); RelativeVec toVec = context.get("to"); Vec from = fromVec.from(player.getPosition()); Vec to = toVec.from(player.getPosition()); int startX = Math.min(from.blockX(), to.blockX()); int endX = Math.max(from.blockX(), to.blockX()); int startZ = Math.min(from.blockZ(), to.blockZ()); int endZ = Math.max(from.blockZ(), to.blockZ()); Instance instance = player.getInstance(); for (int offX = startX; offX < endX; offX += 16) { for (int offZ = startZ; offZ < endZ; offZ += 16) { // Get chunk position int chunkX = CoordConversion.globalToChunk(offX); int chunkZ = CoordConversion.globalToChunk(offZ); removeForceLoad(instance, chunkX, chunkZ); } } } private void usageRemoveFrom(CommandSender sender, CommandContext context) { if (!(sender instanceof Player player)) { sender.sendMessage("This command must be executed by a player!"); return; } RelativeVec fromVec = context.get("from"); Vec position = fromVec.from(player.getPosition()); // Get chunk position int chunkX = CoordConversion.globalToChunk(position.x()); int chunkZ = CoordConversion.globalToChunk(position.z()); // Remove force load Instance instance = player.getInstance(); removeForceLoad(instance, chunkX, chunkZ); } private void usageRemoveFromTo(CommandSender sender, CommandContext context) { if (!(sender instanceof Player player)) { sender.sendMessage("This command must be executed by a player!"); return; } RelativeVec fromVec = context.get("from"); RelativeVec toVec = context.get("to"); Vec from = fromVec.from(player.getPosition()); Vec to = toVec.from(player.getPosition()); int minX = Math.min(from.blockX(), to.blockX()); int maxX = Math.max(from.blockX(), to.blockX()); int minZ = Math.min(from.blockZ(), to.blockZ()); int maxZ = Math.max(from.blockZ(), to.blockZ()); Instance instance = player.getInstance(); for (int offX = minX; offX <= maxX; offX += 16) { for (int offZ = minZ; offZ <= maxZ; offZ += 16) { // Get chunk position int chunkX = CoordConversion.globalToChunk(offX); int chunkZ = CoordConversion.globalToChunk(offZ); // Remove the force load removeForceLoad(instance, chunkX, chunkZ); } } } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/GamemodeCommand.java ================================================ package net.minestom.vanilla.commands; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.arguments.ArgumentEnum; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity; import net.minestom.server.entity.Entity; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.utils.entity.EntityFinder; import java.util.List; import java.util.Locale; /** * Command that make a player change gamemode, made in * the style of the vanilla /gamemode command. * * @see ... */ public class GamemodeCommand extends Command { public GamemodeCommand() { super("gamemode", "gm"); //GameMode parameter ArgumentEnum gamemode = ArgumentType.Enum("gamemode", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED); gamemode.setCallback((sender, exception) -> sender.sendMessage( Component.text("Invalid gamemode ", NamedTextColor.RED) .append(Component.text(exception.getInput(), NamedTextColor.WHITE)) .append(Component.text("!")))); ArgumentEntity player = ArgumentType.Entity("targets").onlyPlayers(true); //Upon invalid usage, print the correct usage of the command to the sender setDefaultExecutor((sender, context) -> { String commandName = context.getCommandName(); sender.sendMessage(Component.text("Usage: /" + commandName + " [targets]", NamedTextColor.RED)); }); //Command Syntax for /gamemode addSyntax((sender, context) -> { //Limit execution to players only if (!(sender instanceof Player playerSender)) { sender.sendMessage(Component.text("Please run this command in-game.", NamedTextColor.RED)); return; } //Check permission, this could be replaced with hasPermission if (playerSender.getPermissionLevel() < 2) { sender.sendMessage(Component.text("You don't have permission to use this command.", NamedTextColor.RED)); return; } GameMode mode = context.get(gamemode); //Set the gamemode for the sender executeSelf(playerSender, mode); }, gamemode); //Command Syntax for /gamemode [targets] addSyntax((sender, context) -> { //Check permission for players only //This allows the console to use this syntax too if ((sender instanceof Player playerSender) && playerSender.getPermissionLevel() < 2) { sender.sendMessage(Component.text("You don't have permission to use this command.", NamedTextColor.RED)); return; } EntityFinder finder = context.get(player); GameMode mode = context.get(gamemode); //Set the gamemode for the targets executeOthers(sender, mode, finder.find(sender)); }, gamemode, player); } /** * Sets the gamemode for the specified entities, and * notifies them (and the sender) in the chat. */ private void executeOthers(CommandSender sender, GameMode mode, List entities) { if (entities.isEmpty()) { //If there are no players that could be modified, display an error message if (sender instanceof Player playerSender) sender.sendMessage(Component.translatable("argument.entity.notfound.player", NamedTextColor.RED)); else sender.sendMessage(Component.text("No player was found", NamedTextColor.RED)); } else for (Entity entity : entities) { if (entity instanceof Player p) { if (p == sender) { //If the player is the same as the sender, call //executeSelf to display one message instead of two executeSelf(p, mode); } else { p.setGameMode(mode); String gamemodeString = "gameMode." + mode.name().toLowerCase(Locale.ROOT); Component gamemodeComponent = Component.translatable(gamemodeString); Component playerName = p.getDisplayName() == null ? p.getName() : p.getDisplayName(); //Send a message to the changed player and the sender p.sendMessage(Component.translatable("gameMode.changed", gamemodeComponent)); sender.sendMessage(Component.translatable("commands.gamemode.success.other", playerName, gamemodeComponent)); } } } } /** * Sets the gamemode for the executing Player, and * notifies them in the chat. */ private void executeSelf(Player sender, GameMode mode) { sender.setGameMode(mode); //The translation keys 'gameMode.survival', 'gameMode.creative', etc. //correspond to the translated game mode names. String gamemodeString = "gameMode." + mode.name().toLowerCase(Locale.ROOT); Component gamemodeComponent = Component.translatable(gamemodeString); //Send the translated message to the player. sender.sendMessage(Component.translatable("commands.gamemode.success.self", gamemodeComponent)); } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/HelpCommand.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Returns the list of all available commands */ public class HelpCommand extends Command { public HelpCommand() { super("help"); setDefaultExecutor(this::execute); } private void execute(CommandSender sender, CommandContext context) { sender.sendMessage("=== Help ==="); List commands = new ArrayList<>(); Collections.addAll(commands, VanillaCommands.values()); commands.sort(this::compareCommands); commands.forEach(command -> sender.sendMessage("/" + command.name().toLowerCase())); sender.sendMessage("============"); } private int compareCommands(VanillaCommands a, VanillaCommands b) { return a.name().compareTo(b.name()); } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/MeCommand.java ================================================ package net.minestom.vanilla.commands; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.minestom.server.adventure.audience.Audiences; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.arguments.ArgumentStringArray; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.entity.Player; /** * Command that displays a player action */ public class MeCommand extends Command { public MeCommand() { super("me"); setDefaultExecutor(this::usage); ArgumentStringArray message = ArgumentType.StringArray("message"); addSyntax(this::execute, message); } private void usage(CommandSender player, CommandContext arguments) { player.sendMessage("Usage: /me "); } private void execute(CommandSender sender, CommandContext arguments) { if (!(sender instanceof Player player)) { sender.sendMessage("This command must be executed by a player!"); return; } String[] messageParts = arguments.get("message"); TextComponent.Builder builder = Component.text(); builder.append(Component.text(" * " + player.getUsername())); builder.append(Component.text(" ")); builder.append(Component.text(messageParts[0])); for (int i = 1; i < messageParts.length; i++) { builder.append(Component.text(messageParts[i])); } Component message = builder.build(); Audiences.all().sendMessage(message); } @SuppressWarnings("unused") private boolean isAllowed(CommandSender player) { return player instanceof Player; // TODO: permissions } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/SaveAllCommand.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; import net.minestom.vanilla.logging.Logger; /** * Save the server */ public class SaveAllCommand extends Command { public SaveAllCommand() { super("save-all"); setCondition(this::condition); setDefaultExecutor(this::execute); } private boolean condition(CommandSender player, String commandName) { return true; // TODO: permissions } private void execute(CommandSender player, CommandContext arguments) { MinecraftServer.getInstanceManager().getInstances().forEach(i -> { i.saveChunksToStorage(); Logger.info("Saved dimension " + i.getDimensionType().name()); }); } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/StopCommand.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.CommandContext; /** * Stops the server */ public class StopCommand extends Command { public StopCommand() { super("stop"); setCondition(this::condition); setDefaultExecutor(this::execute); } private boolean condition(CommandSender player, String commandName) { return true; // TODO: permissions } private void execute(CommandSender player, CommandContext arguments) { MinecraftServer.stopCleanly(); } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/VanillaCommands.java ================================================ package net.minestom.vanilla.commands; import net.minestom.server.command.CommandManager; import net.minestom.server.command.builder.Command; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * All commands available in the vanilla reimplementation */ public enum VanillaCommands { FORCELOAD(ForceloadCommand::new), GAMEMODE(GamemodeCommand::new), DIFFICULTY(DifficultyCommand::new), ME(MeCommand::new), STOP(StopCommand::new), HELP(HelpCommand::new), SAVE_ALL(SaveAllCommand::new), ; private final Supplier commandCreator; VanillaCommands(Supplier commandCreator) { this.commandCreator = commandCreator; } /** * Register all vanilla commands into the given manager * * @param manager the command manager to register commands on */ public static void registerAll(@NotNull CommandManager manager) { for (VanillaCommands vanillaCommand : values()) { Command command = vanillaCommand.commandCreator.get(); manager.register(command); } } } ================================================ FILE: commands/src/main/java/net/minestom/vanilla/commands/VanillaCommandsFeature.java ================================================ package net.minestom.vanilla.commands; import net.kyori.adventure.key.Key; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.instancemeta.InstanceMetaFeature; import org.jetbrains.annotations.NotNull; import java.util.Set; public class VanillaCommandsFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { new Logic().hook(context.vri()); } @Override public @NotNull Key key() { return Key.key("vri:commands"); } private static class Logic { private Logic() { } private void hook(@NotNull VanillaReimplementation vri) { VanillaCommands.registerAll(vri.process().command()); } } @Override public @NotNull Set> dependencies() { return Set.of(InstanceMetaFeature.class); } } ================================================ FILE: core/build.gradle.kts ================================================ dependencies { // Minestom api("net.minestom:minestom:${project.property("minestom_version")}") // Raycasting api("com.github.EmortalMC:Rayfast:${project.property("rayfast_version")}") // Noise api("com.github.Articdive:JNoise:${project.property("jnoise_version")}") // Annotations api("org.jetbrains:annotations:${project.property("annotations_version")}") // SLF4j api("org.slf4j:slf4j-api:${project.property("slf4j_version")}") // Json api("com.squareup.moshi:moshi:1.14.0") api("com.squareup.moshi:moshi-adapters:1.14.0") // Tests testImplementation(platform("org.junit:junit-bom:5.9.1")) testImplementation("org.junit.jupiter:junit-jupiter") } tasks.test { useJUnitPlatform() } ================================================ FILE: core/src/main/java/net/minestom/vanilla/VanillaRegistry.java ================================================ package net.minestom.vanilla; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.tag.TagReadable; import org.jetbrains.annotations.NotNull; /** * This registry object is used to register data and logic. */ public sealed interface VanillaRegistry permits VanillaReimplementationImpl.VanillaRegistryImpl { /** * Registers an entity type to its entity spawner. * * @param type the type of the entity * @param supplier the entity spawner of the entity */ void register(@NotNull EntityType type, @NotNull EntitySpawner supplier); interface EntitySpawner { @NotNull Entity spawn(@NotNull VanillaRegistry.EntityContext context); } interface EntityContext extends TagReadable { @NotNull EntityType type(); @NotNull Pos position(); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/VanillaReimplementation.java ================================================ package net.minestom.vanilla; import net.kyori.adventure.key.Key; import net.minestom.server.ServerProcess; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.Instance; import net.minestom.server.tag.TagWritable; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.logging.StatusUpdater; import net.minestom.vanilla.utils.DependencySorting; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Random; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; public interface VanillaReimplementation { /** * Creates a new instance of {@link VanillaReimplementation} and hooks into the server process. * * @param process the server process * @return the new instance */ static @NotNull VanillaReimplementation hook(@NotNull ServerProcess process) { return VanillaReimplementation.hook(process, feature -> true); } /** * Creates a new instance of {@link VanillaReimplementation} and hooks all features into the server process. *

* This method only hooks the features that pass the given predicate. *

* * @param process the server process * @param predicate the predicate to test the features * @return the new instance */ static @NotNull VanillaReimplementation hook(@NotNull ServerProcess process, Predicate predicate) { return VanillaReimplementationImpl.hook(process, predicate); } // Vri Methods /** * @return the server process */ @NotNull ServerProcess process(); /** * Acquires the given {@link Feature} from this reimplementation. * @param clazz the feature's class * @param the feature's type * @return the feature */ @NotNull T feature(Class clazz); /** * Creates an {@link net.minestom.vanilla.VanillaRegistry.EntityContext} for the given type and position * * @param type the type of the entity * @param position the position of the entity at spawn * @return the context */ default @NotNull VanillaRegistry.EntityContext entityContext(EntityType type, Point position) { return entityContext(type, position, writer -> { }); } /** * Creates an {@link net.minestom.vanilla.VanillaRegistry.EntityContext} for the given type and position, with the * given tag values. * * @param type the type of the entity * @param position the position of the entity at spawn * @return the context */ @NotNull VanillaRegistry.EntityContext entityContext(EntityType type, Point position, @NotNull Consumer tagWriter); /** * Creates a new vanilla entity, using the specified context, returning null if the entity type is not implemented. * * @param context the context * @return the new entity */ @Nullable Entity createEntity(@NotNull VanillaRegistry.EntityContext context); /** * Creates a new vanilla entity, using the specified context, returning a dummy entity if the entity type is not * implemented. * * @param context the context * @return the new entity */ @NotNull Entity createEntityOrDummy(@NotNull VanillaRegistry.EntityContext context); /** * Creates and registers a vanilla instance. */ @NotNull Instance createInstance(@NotNull Key namespace, @NotNull DimensionType dimension); /** * Gets a registered vanilla instance. * * @param namespace the namespace of the instance * @return the instance, or null if not found */ @Nullable Instance getInstance(Key namespace); /** * Retrieves or generates a random object unique to the given object. *
* Note that this method does not keep the given key in memory, however it does always return the same random for * any given (equal) key object. * * @param key the key * @return the random */ @NotNull Random random(@NotNull Object key); /** * A feature is a collection of logic that can be hooked into a server process. */ interface Feature extends DependencySorting.NamespaceDependent> { /** * Hooks into this server process. *

* DO NOT manually call this method, use {@link VanillaReimplementation#hook} instead. *

* * @param vri the vanilla reimplementation object * @param registry the registry object */ @Deprecated default void hook(@NotNull VanillaReimplementation vri, @NotNull VanillaRegistry registry) { } /** * Hooks into this server process. *

* DO NOT manually call this method, use {@link VanillaReimplementation#hook} instead. *

* * @param context the context containing all related objects */ void hook(@NotNull HookContext context); interface HookContext { @NotNull VanillaReimplementation vri(); @NotNull VanillaRegistry registry(); @NotNull StatusUpdater status(); } /** * Obtains the dependencies of this feature. * These dependencies will be loaded before this feature. * @return the dependencies */ default @NotNull Set> dependencies() { return Set.of(); } /** * @return a unique {@link Key} for this feature */ @NotNull Key key(); /** * @return a unique {@link Class} for this feature */ default @NotNull Class identity() { return getClass(); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/VanillaReimplementationImpl.java ================================================ package net.minestom.vanilla; import net.kyori.adventure.key.Key; import net.minestom.server.ServerProcess; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.LightingChunk; import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.registry.RegistryKey; import net.minestom.server.tag.TagHandler; import net.minestom.server.tag.TagWritable; import net.minestom.server.tag.Taggable; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.dimensions.VanillaDimensionTypes; import net.minestom.vanilla.instance.SetupVanillaInstanceEvent; import net.minestom.vanilla.logging.Loading; import net.minestom.vanilla.logging.Logger; import net.minestom.vanilla.logging.StatusUpdater; import net.minestom.vanilla.utils.DependencySorting; import net.minestom.vanilla.utils.MinestomUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; class VanillaReimplementationImpl implements VanillaReimplementation { private final ServerProcess process; private final Map worlds = new ConcurrentHashMap<>(); private final Map entity2Spawner = new ConcurrentHashMap<>(); private final Map, Feature> class2Feature = new ConcurrentHashMap<>(); private final Map randoms = Collections.synchronizedMap(new WeakHashMap<>()); private VanillaReimplementationImpl(@NotNull ServerProcess process) { this.process = process; } /** * Creates a new instance of {@link VanillaReimplementationImpl} and hooks into the server process. * * @param process the server process * @param predicate a predicate to determine which features to enable * @return the new instance */ public static @NotNull VanillaReimplementationImpl hook(@NotNull ServerProcess process, Predicate predicate) { Loading.start("Initialising"); Loading.start("Initialising Minestom Resources..."); MinestomUtils.initialize(); Loading.finish(); Loading.updater().progress(0.33); Loading.start("Instantiating vri"); VanillaReimplementationImpl vri = new VanillaReimplementationImpl(process); Loading.finish(); Loading.updater().progress(0.66); vri.INTERNAL_HOOK(predicate); Loading.updater().progress(1); Loading.finish(); return vri; } // Vri Methods /** * @return the server process */ public @NotNull ServerProcess process() { return process; } @Override public @NotNull T feature(Class clazz) { //noinspection unchecked return (T) Objects.requireNonNull(class2Feature.get(clazz), () -> "Feature " + clazz + " has not loaded yet."); } /** * Creates an {@link VanillaRegistry.EntityContext} for the given type and position * * @param type the type of the entity * @param position the position of the entity at spawn * @return the context */ public @NotNull VanillaRegistry.EntityContext entityContext(EntityType type, Point position) { return new EntityContextImpl(type, Pos.fromPoint(position)); } /** * Creates an {@link VanillaRegistry.EntityContext} for the given type and position, with the * given tag values. * * @param type the type of the entity * @param position the position of the entity at spawn * @return the context */ public @NotNull VanillaRegistry.EntityContext entityContext(EntityType type, Point position, @NotNull Consumer tagWriter) { EntityContextImpl impl = new EntityContextImpl(type, Pos.fromPoint(position)); tagWriter.accept(impl); return impl; } private record EntityContextImpl(@NotNull EntityType type, @NotNull Pos position, @NotNull TagHandler tagHandler) implements VanillaRegistry.EntityContext, Taggable { public EntityContextImpl(@NotNull EntityType type, @NotNull Pos position) { this(type, position, TagHandler.newHandler()); } } /** * Creates a new vanilla entity, using the specified context, returning null if the entity type is not implemented. * * @param context the context * @return the new entity */ public @Nullable Entity createEntity(@NotNull VanillaRegistry.EntityContext context) { // Get the spawner VanillaRegistry.EntitySpawner spawner = entity2Spawner.get(context.type()); // Create the entity return spawner != null ? spawner.spawn(context) : null; } /** * Creates a new vanilla entity, using the specified context, returning a dummy entity if the entity type is not * implemented. * * @param context the context * @return the new entity */ public @NotNull Entity createEntityOrDummy(@NotNull VanillaRegistry.EntityContext context) { Entity entity = createEntity(context); return entity != null ? entity : new DummyEntity(context.type()); } private static class DummyEntity extends Entity { public DummyEntity(@NotNull EntityType type) { super(type); } } /** * Creates a vanilla instance. */ public @NotNull Instance createInstance(@NotNull Key name, @NotNull DimensionType dimension) { RegistryKey key = process().dimensionType().getKey(dimension); Objects.requireNonNull(key, "Dimension type " + dimension + " is not registered!"); InstanceContainer instance = process().instance().createInstanceContainer(key); worlds.put(name, instance); // Anvil directory AnvilLoader loader = new AnvilLoader(name.value()); instance.setChunkLoader(loader); instance.setChunkSupplier(LightingChunk::new); // Setup event SetupVanillaInstanceEvent event = new SetupVanillaInstanceEvent(instance); process().eventHandler().call(event); return instance; } @Override public @NotNull Instance getInstance(Key dimensionId) { return worlds.get(dimensionId); } @Override public @NotNull Random random(@NotNull Object key) { return randoms.computeIfAbsent(key, k -> new Random(key.hashCode())); } final class VanillaRegistryImpl implements VanillaRegistry { @Override public void register(@NotNull EntityType type, @NotNull EntitySpawner supplier) { entity2Spawner.put(type, supplier); } } private void INTERNAL_HOOK(Predicate predicate) { // Create the registry VanillaRegistry registry = new VanillaRegistryImpl(); // Hook this core library Loading.start("Hooking Core Library"); hookCoreLibrary(); Loading.finish(); // Load all the features and hook them Loading.start("Loading features from classpath"); Set features = ServiceLoader.load(Feature.class) .stream() .map(ServiceLoader.Provider::get) .collect(Collectors.toUnmodifiableSet()); Loading.finish(); Loading.start("Validating dependencies"); for (Feature feature : features) { try { for (Class dependency : feature.dependencies()) { Objects.requireNonNull(dependency, "Dependency cannot be null!"); } } catch (Exception e) { Logger.error("Failed to load features! Does one of your features have a missing dependency feature?", e); throw new RuntimeException(e); } } Loading.finish(); Loading.start("Sorting features by dependencies"); List sortedByDependencies = DependencySorting.sort(features); Loading.finish(); for (Feature feature : sortedByDependencies) { if (!predicate.test(feature)) { Logger.info("Skipping feature %s...", feature.key()); continue; } try { instructHook(feature, registry); //noinspection unchecked class2Feature.put((Class) feature.getClass(), feature); } catch (Exception e) { Logger.error("Failed to load feature: " + feature.key(), e); throw new RuntimeException(e); } } } private void instructHook(Feature feature, VanillaRegistry registry) { try { Loading.start("" + feature.key()); Feature.HookContext context = new HookContextImpl(this, registry, Loading.updater()); feature.hook(context); } catch (Exception e) { Logger.error(e, "Failed to load feature: %s%n", feature.key()); throw new RuntimeException(e); } finally { Loading.finish(); } } private record HookContextImpl(VanillaReimplementation vri, VanillaRegistry registry, StatusUpdater status) implements Feature.HookContext { } private void hookCoreLibrary() { VanillaDimensionTypes.registerAll(process().dimensionType()); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/dimensions/VanillaDimensionTypes.java ================================================ package net.minestom.vanilla.dimensions; import net.kyori.adventure.key.Key; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.world.DimensionType; import java.util.Map; public class VanillaDimensionTypes { public static final DimensionType OVERWORLD = DimensionType.builder() .build(); public static Map values() { return Map.of( OVERWORLD, Key.key("vri:overworld") ); } public static void registerAll(DynamicRegistry registry) { values().forEach((dimensionType, namespaceID) -> { registry.register(namespaceID, dimensionType); }); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/events/BlastingFurnaceTickEvent.java ================================================ package net.minestom.vanilla.events; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.event.Event; import net.minestom.server.event.trait.BlockEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; public record BlastingFurnaceTickEvent(Block getBlock, Instance getInstance, BlockVec getBlockPosition, Inventory getInventory) implements Event, InstanceEvent, BlockEvent, InventoryEvent { } ================================================ FILE: core/src/main/java/net/minestom/vanilla/events/FurnaceTickEvent.java ================================================ package net.minestom.vanilla.events; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.event.Event; import net.minestom.server.event.trait.BlockEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; public record FurnaceTickEvent(Block getBlock, Instance getInstance, BlockVec getBlockPosition, Inventory getInventory) implements Event, InstanceEvent, BlockEvent, InventoryEvent { } ================================================ FILE: core/src/main/java/net/minestom/vanilla/events/SmokerTickEvent.java ================================================ package net.minestom.vanilla.events; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.event.Event; import net.minestom.server.event.trait.BlockEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; public record SmokerTickEvent(Block getBlock, Instance getInstance, BlockVec getBlockPosition, Inventory getInventory) implements Event, InstanceEvent, BlockEvent, InventoryEvent { } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/ByteArray.java ================================================ package net.minestom.vanilla.files; import net.minestom.server.utils.MathUtils; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; public class ByteArray { private final byte[] bytes; private ByteArray(byte[] b, boolean copy) { this.bytes = copy ? deepCopy(b) : b; } public static ByteArray wrap(byte[] bytes) { return new ByteArray(bytes, false); } public static ByteArray copyOf(byte[] bytes) { return new ByteArray(bytes, true); } public byte[] array() { return deepCopy(bytes); } public int size() { return bytes.length; } public byte index(int i) { if (!MathUtils.isBetween(i, 0, size())) throw new ArrayIndexOutOfBoundsException(); return bytes[i]; } public InputStream toStream() { return new ByteArrayInputStream(bytes); } public String toCharacterString() { return toCharacterString(StandardCharsets.UTF_8); } public String toCharacterString(Charset charset) { return new String(bytes, charset); } private byte[] deepCopy(byte[] source) { byte[] copy = new byte[source.length]; System.arraycopy(source, 0, copy, 0, source.length); return copy; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ByteArray byteArray = (ByteArray) o; return Arrays.equals(bytes, byteArray.bytes); } @Override public int hashCode() { return Arrays.hashCode(bytes); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/CacheFileSystem.java ================================================ package net.minestom.vanilla.files; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; class CacheFileSystem implements FileSystemImpl { static final CacheFileSystem EMPTY = new CacheFileSystem<>(new DynamicFileSystem<>()); private final Map files; private final Map> folders; protected CacheFileSystem(FileSystem original) { this.files = Map.copyOf(original.files().stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), original::file))); this.folders = original.folders().stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), name -> original.folder(name).cache())); } @Override public Set folders() { return folders.keySet(); } @Override public Set files() { return files.keySet(); } @Override public FileSystem folder(String path) { return folders.getOrDefault(path, FileSystem.empty()); } @Override public F file(String path) { return files.get(path); } @Override public String toString() { return FileSystemImpl.toString(this); } @Override public FileSystem cache() { return this; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/DynamicFileSystem.java ================================================ package net.minestom.vanilla.files; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; public class DynamicFileSystem implements FileSystemImpl { protected final Map files = new ConcurrentHashMap<>(); protected final Map> folders = new ConcurrentHashMap<>(); protected DynamicFileSystem() {} static FileSystem from(FileSystemImpl fileSystem) { DynamicFileSystem dynamicFileSystem = new DynamicFileSystem<>(); for (String folder : fileSystem.folders()) { FileSystemImpl subFileSystem = (FileSystemImpl) fileSystem.folder(folder); dynamicFileSystem.folders.put(folder, (DynamicFileSystem) from(subFileSystem)); } for (Map.Entry entry : fileSystem.files().stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), fileSystem::file)).entrySet()) { dynamicFileSystem.addFile(entry.getKey(), entry.getValue()); } return dynamicFileSystem; } public DynamicFileSystem addFolder(String directoryName) { int split = directoryName.contains("/") ? directoryName.indexOf('/') : directoryName.length(); String folderName = directoryName.substring(0, split); DynamicFileSystem fileSource = folders.computeIfAbsent(folderName, s -> new DynamicFileSystem<>()); String remaining = directoryName.substring(directoryName.indexOf("/") + 1); if (remaining.contains("/")) { fileSource.addFolder(remaining); } return fileSource; } public void addFile(String name, F contents) { if (!name.contains("/")) { files.put(name, contents); return; } String folderName = name.substring(0, name.indexOf("/")); DynamicFileSystem newFileSource = addFolder(folderName); String remaining = name.substring(name.indexOf("/") + 1); newFileSource.addFile(remaining, contents); } @Override public Set folders() { return folders.keySet(); } @Override public Set files() { return files.keySet(); } @Override public FileSystem folder(@NotNull String path) { var fs = folders.get(path); return fs == null ? FileSystem.empty() : fs; } @Override public F file(String path) { return files.get(path); } @Override public String toString() { return FileSystemImpl.toString(this); } @Override public FileSystem inMemory() { return this; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/FileSystem.java ================================================ package net.minestom.vanilla.files; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; public interface FileSystem extends FileSystemMappers { /** * Queries all the folders in the given directory and returns a set of folder names. * * @return a set of folder names (no directory prefix) */ Set folders(); /** * Queries all the files in the given directory and returns a set of file names. * * @return a set of file names (no directory prefix) */ Set files(); /** * Navigates to the given subdirectory and returns a new FileSource for that directory. * * @param path the path to the subdirectory * @return a new FileSource for the subdirectory */ FileSystem folder(String path); /** * Reads the file at the given path and returns its contents. * @param path the path to the file * @return the contents of the file */ F file(String path); FileSystem folder(@NotNull String... paths); FileSystem map(Function mapper); FileSystem map(BiFunction mapper); FileSystem cache(); FileSystem lazy(); FileSystem inMemory(); static FileSystem empty() { //noinspection unchecked return (FileSystem) CacheFileSystem.EMPTY; } static FileSystem fromZipFile(File file, Predicate pathFilter) { return FileSystemUtil.unzipIntoFileSystem(file, pathFilter); } default boolean hasFile(String file) { return files().contains(file); } default boolean hasFolder(String path) { return folders().contains(path); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/FileSystemImpl.java ================================================ package net.minestom.vanilla.files; import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.Iterator; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; interface FileSystemImpl extends FileSystem { default FileSystem folder(@NotNull String... paths) { if (paths.length == 0) return this; Iterator iter = Arrays.stream(paths).iterator(); FileSystem fs = folder(iter.next()); while (iter.hasNext()) { fs = fs.folder(iter.next()); if (fs.folders().isEmpty()) return fs; } return fs; } default FileSystem map(Function mapper) { return map((str, file) -> mapper.apply(file)); } default FileSystem map(BiFunction mapper) { return new MappedFileSystem<>(this, mapper); } default FileSystem cache() { return new CacheFileSystem<>(this); } default FileSystem lazy() { return new LazyFileSystem<>(this); } default FileSystem inMemory() { return DynamicFileSystem.from(this); } static String toString(FileSystem fs) { Stream.Builder builder = Stream.builder(); toString(fs, builder, 0); return builder.build().collect(Collectors.joining()); } static void toString(FileSystem fs, Stream.Builder builder, int depth) { String prefix = " ".repeat(depth); String folderChar = "\uD83D\uDDC0"; String fileChar = "\uD83D\uDDCE"; for (String folder : fs.folders()) { builder.add(prefix); builder.add(folderChar); builder.add(" "); builder.add(folder); builder.add("\n"); toString(fs.folder(folder), builder, depth + 1); } for (String file : fs.files()) { builder.add(prefix); builder.add(fileChar); builder.add(" "); builder.add(file); builder.add("\n"); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/FileSystemMappers.java ================================================ package net.minestom.vanilla.files; import com.google.gson.JsonElement; import java.io.InputStream; import java.util.function.Function; interface FileSystemMappers { Function INPUT_STREAM_TO_BYTES = inputStream -> { try { return ByteArray.wrap(inputStream.readAllBytes()); } catch (Exception e) { throw new RuntimeException(e); } }; Function BYTES_TO_STRING = ByteArray::toCharacterString; Function STRING_TO_JSON = string -> FileSystemUtil.gson.fromJson(string, JsonElement.class); Function INPUT_STREAM_TO_STRING = INPUT_STREAM_TO_BYTES.andThen(BYTES_TO_STRING); Function INPUT_STREAM_TO_JSON = INPUT_STREAM_TO_STRING.andThen(STRING_TO_JSON); Function BYTES_TO_JSON = BYTES_TO_STRING.andThen(STRING_TO_JSON); } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/FileSystemUtil.java ================================================ package net.minestom.vanilla.files; import com.google.gson.Gson; import com.google.gson.JsonElement; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.function.Predicate; import java.util.regex.PatternSyntaxException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; class FileSystemUtil { static FileSystem toBytes(FileSystem source) { return source.map(inputStream -> { try { return ByteArray.copyOf(inputStream.readAllBytes()); } catch (IOException e) { throw new RuntimeException(e); } }); } static FileSystem toString(FileSystem source) { return toBytes(source).map(ByteArray::toString); } static FileSystem toJson(FileSystem source) { Gson gson = new Gson(); return toString(source).map(str -> gson.fromJson(str, JsonElement.class)); } static DynamicFileSystem unzipIntoFileSystem(@NotNull File file, Predicate filter) { DynamicFileSystem source = new DynamicFileSystem<>(); try (ZipInputStream in = new ZipInputStream(new FileInputStream(file))) { ZipEntry entry; while ((entry = in.getNextEntry()) != null) { String name = entry.getName(); if (!filter.test(name)) continue; if (entry.isDirectory() || name.endsWith("\\")) { source.addFolder(name); } else { source.addFile(name, ByteArray.copyOf(in.readAllBytes())); } } } catch (IOException | PatternSyntaxException e) { throw new RuntimeException(e); } return source; } static final Gson gson = new Gson(); } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/LazyFileSystem.java ================================================ package net.minestom.vanilla.files; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * A FileSystem that lazily loads its contents. * @param */ public class LazyFileSystem implements FileSystemImpl { private final FileSystem original; protected LazyFileSystem(FileSystem original) { this.original = original; } private Set folders = null; @Override public Set folders() { if (folders == null) { folders = Set.copyOf(original.folders()); } return folders; } private Set files = null; @Override public Set files() { if (files == null) { files = Set.copyOf(original.files()); } return files; } private final Map> folderCache = new ConcurrentHashMap<>(); @Override public FileSystem folder(String path) { return folderCache.computeIfAbsent(path, original::folder); } private final Map fileCache = new ConcurrentHashMap<>(); @Override public F file(String path) { return fileCache.computeIfAbsent(path, original::file); } @Override public String toString() { return FileSystemImpl.toString(this); } @Override public FileSystem lazy() { return this; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/MappedFileSystem.java ================================================ package net.minestom.vanilla.files; import java.util.Set; import java.util.function.BiFunction; class MappedFileSystem implements FileSystemImpl { private final FileSystem original; private final BiFunction mapper; protected MappedFileSystem(FileSystem original, BiFunction mapper) { this.original = original; this.mapper = mapper; } @Override public Set folders() { return original.folders(); } @Override public Set files() { return original.files(); } @Override public FileSystemImpl folder(String path) { return new MappedFileSystem<>(original.folder(path), mapper); } @Override public T file(String path) { return mapper.apply(path, original.file(path)); } @Override public String toString() { return FileSystemImpl.toString(this); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/files/PathFileSystem.java ================================================ package net.minestom.vanilla.files; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; class PathFileSystem implements FileSystemImpl { private final Path path; protected PathFileSystem(Path path) { this.path = path; } @Override public Set folders() { // Return all folders in the path directory (Only this directory, not subdirectories) try (Stream paths = Files.walk(this.path, 0)) { return paths .filter(Files::isDirectory) .map(path -> path.getFileName().toString()) .collect(Collectors.toUnmodifiableSet()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public Set files() { // Return all files in the path directory (Only this directory, not subdirectories) try (Stream paths = Files.walk(this.path, 0)) { return paths .filter(Files::isRegularFile) .map(path -> path.getFileName().toString()) .collect(Collectors.toUnmodifiableSet()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public PathFileSystem folder(String path) { return new PathFileSystem(this.path.resolve(path)); } @Override public ByteArray file(String path) { try { return ByteArray.wrap(Files.readAllBytes(this.path.resolve(path))); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String toString() { return FileSystemImpl.toString(this); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/instance/SetupVanillaInstanceEvent.java ================================================ package net.minestom.vanilla.instance; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; public class SetupVanillaInstanceEvent implements InstanceEvent { private final Instance instance; public SetupVanillaInstanceEvent(@NotNull Instance instance) { this.instance = instance; } @Override public @NotNull Instance getInstance() { return instance; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/instance/VanillaExplosion.java ================================================ package net.minestom.vanilla.instance; import dev.emortal.rayfast.area.Intersection; import dev.emortal.rayfast.area.area3d.Area3d; import dev.emortal.rayfast.casting.grid.GridCast; import dev.emortal.rayfast.vector.Vector3d; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Explosion; import net.minestom.server.instance.Instance; import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.time.TimeUnit; import java.util.*; public class VanillaExplosion extends Explosion { public static final String DROP_EVERYTHING_KEY = "minestom:drop_everything"; public static final String IS_FLAMING_KEY = "minestom:is_flaming"; public static final String DONT_DESTROY_BLOCKS_KEY = "minestom:no_block_damage"; private static final Random explosionRNG = new Random(); private final boolean startsFires; private final boolean dropsEverything; public static final String THREAD_POOL_NAME = "MSVanilla-Explosion"; public static final int THREAD_POOL_COUNT = 2; private final Point center; private final boolean blockDamage; protected VanillaExplosion(Point center, float strength, boolean dropEverything, boolean isFlaming, boolean dontDestroyBlocks) { super((float) center.x(), (float) center.y(), (float) center.z(), strength); this.center = center; this.blockDamage = dropEverything; this.startsFires = isFlaming; this.dropsEverything = dontDestroyBlocks; } public static Builder builder(Point center, float strength) { return new Builder(center, strength); } @Override protected List prepare(Instance instance) { float maximumBlastRadius = getStrength(); Set positions = new HashSet<>(); if (blockDamage) { for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { if (!(x == 0 || x == 15 || y == 0 || y == 15 || z == 0 || z == 15)) { // must be on outer edge of 16x16x16 cube continue; } Vec dir = new Vec(x - 8.5f, y - 8.5f, z - 8.5f).normalize(); Iterator gridIterator = GridCast.createGridIterator(getCenterX(), getCenterY(), getCenterZ(), dir.x(), dir.y(), dir.z(), 1.0, maximumBlastRadius); double intensity = (0.7f + explosionRNG.nextFloat() * 0.6f) * getStrength(); while (gridIterator.hasNext()) { Vector3d vec = gridIterator.next(); Point pos = new Vec(vec.x(), vec.y(), vec.z()); intensity -= 0.225; Block block = Objects.requireNonNull(instance.loadOptionalChunk(pos).join()).getBlock(pos); double explosionResistance = block.registry().explosionResistance(); intensity -= (explosionResistance / 5.0); if (intensity < 0) { break; } positions.add(pos); } } } } } final float damageRadius = maximumBlastRadius; // TODO: should be different from blast radius List potentiallyDamagedEntities = getEntitiesAround(instance, damageRadius); for (Entity entity : potentiallyDamagedEntities) { affect(entity, damageRadius); } if (blockDamage) { for (Point position : positions) { Block block = instance.getBlock(position); if (block.isAir()) { continue; } // if (block.compare(Block.TNT)) { // spawnPrimedTNT(instance, position, new Pos(getCenterX(), getCenterY(), getCenterZ())); // continue; // } // if (customBlock != null) { // if (!customBlock.onExplode(instance, position, lootTableArguments)) { // continue; // } // } double p = explosionRNG.nextDouble(); boolean shouldDropItem = p <= 1 / getStrength(); if (dropsEverything || shouldDropItem) { // LootTableManager lootTableManager = MinecraftServer.getLootTableManager(); // try { // LootTable table = null; // if (customBlock != null) { // table = customBlock.getLootTable(lootTableManager); // } // if (table == null) { // table = lootTableManager.load(Key.key("blocks/" + block.name().toLowerCase())); // } // List output = table.generate(lootTableArguments); // for (ItemStack out : output) { // ItemEntity itemEntity = new ItemEntity(out, new Position(position.getX() + explosionRNG.nextFloat(), position.getY() + explosionRNG.nextFloat(), position.getZ() + explosionRNG.nextFloat())); // itemEntity.setPickupDelay(500L, TimeUnit.MILLISECOND); // itemEntity.setInstance(instance); // } // } catch (FileNotFoundException e) { // // loot table does not exist, ignore // } } } } return new LinkedList<>(positions); } // private void spawnPrimedTNT(Instance instance, Point blockPosition, Point explosionSource) { // Pos initialPosition = new Pos(blockPosition.blockX() + 0.5f, blockPosition.blockY() + 0f, blockPosition.blockZ() + 0.5f); // // PrimedTNT primedTNT = new PrimedTNT(10 + (TNTBlockHandler.TNT_RANDOM.nextInt(5) - 2)); // primedTNT.setInstance(instance); // primedTNT.teleport(initialPosition); // // Point direction = blockPosition.sub(explosionSource); // double distance = explosionSource.distanceSquared(blockPosition); // Vec vec = new Vec(direction.x(), direction.y(), direction.z()); // vec = vec.div(distance); // // primedTNT.setVelocity(vec.mul(15)); // } @Override protected void postSend(Instance instance, List blocks) { if (!startsFires) { return; } AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); for (Point position : blocks) { Block block = instance.getBlock(position); if (block.isAir() && position.y() > 0) { if (explosionRNG.nextFloat() < 1 / 3f) { Point belowPos = position.add(0, -1, 0); // check that block below is solid Block below = instance.getBlock(belowPos); if (below.isSolid()) { batch.setBlock(position, Block.FIRE); } } } } batch.apply(instance, null); } private void affect(Entity e, final float damageRadius) { double exposure = calculateExposure(e, damageRadius); double distance = e.getPosition().distance(center); double impact = (1.0 - distance / damageRadius) * exposure; double damage = Math.floor((impact * impact + impact) * 7 * getStrength() + 1); if (e instanceof LivingEntity) { ((LivingEntity) e).damage(DamageType.EXPLOSION, (float) damage); } else { if (e instanceof ItemEntity) { e.scheduleRemove(1L, TimeUnit.SERVER_TICK); } // TODO: different entities will react differently (items despawn, boats, minecarts drop as items, etc.) } float blastProtection = 0f; // TODO: apply enchantments exposure -= exposure * 0.15f * blastProtection; Vec velocityBoost = e.getPosition().asVec().add(0f, e.getEyeHeight(), 0f).sub(center); velocityBoost = velocityBoost.normalize().mul(exposure * MinecraftServer.TICK_PER_SECOND); e.setVelocity(e.getVelocity().add(velocityBoost)); } private float calculateExposure(Entity e, final float damageRadius) { int w = (int) (Math.floor(e.getBoundingBox().width() * 2)) + 1; int h = (int) (Math.floor(e.getBoundingBox().height() * 2)) + 1; int d = (int) (Math.floor(e.getBoundingBox().depth() * 2)) + 1; Instance instance = e.getInstance(); Pos pos = e.getPosition(); double entX = pos.x(); double entY = pos.y(); double entZ = pos.z(); // Generate entity hitbox Area3d area3d = Area3d.CONVERTER.from(e); int hits = 0; int rays = w * h * d; int wd2 = w / 2; int dd2 = d / 2; for (int dx = (int) -Math.ceil(wd2); dx < Math.floor(wd2); dx++) { for (int dy = 0; dy < h; dy++) { for (int dz = (int) -Math.ceil(dd2); dz < Math.floor(dd2); dz++) { double deltaX = entX + dx - getCenterX(); double deltaY = entY + dy - getCenterY(); double deltaZ = entZ + dz - getCenterZ(); // TODO: Check for distance Vector3d intersection = area3d.lineIntersection(getCenterX(), getCenterY(), getCenterZ(), deltaX, deltaY, deltaZ, Intersection.ANY_3D); if (intersection != null) { hits++; } } } } return (float) hits / rays; } private List getEntitiesAround(Instance instance, double damageRadius) { int intRadius = (int) Math.ceil(damageRadius); List affected = new LinkedList<>(); double radiusSq = damageRadius * damageRadius; for (int x = -intRadius; x <= intRadius; x++) { for (int z = -intRadius; z <= intRadius; z++) { int posX = (int) Math.floor(getCenterX() + x); int posZ = (int) Math.floor(getCenterZ() + z); var list = instance.getChunkEntities(instance.getChunk(posX >> 4, posZ >> 4)); for (Entity e : list) { Pos pos = e.getPosition(); double dx = pos.x() - getCenterX(); double dy = pos.y() - getCenterY(); double dz = pos.z() - getCenterZ(); if (dx * dx + dy * dy + dz * dz <= radiusSq) { if (!affected.contains(e)) { affected.add(e); } } } } } return affected; } public void trigger(Instance instance) { this.apply(instance); } public static class Builder { private final Point center; private final float strength; private boolean dropEverything = true; private boolean isFlaming = false; private boolean dontDestroyBlocks = false; protected Builder(Point center, float strength) { this.center = center; this.strength = strength; } public Builder dropEverything(boolean dropEverything) { this.dropEverything = dropEverything; return this; } public Builder isFlaming(boolean isFlaming) { this.isFlaming = isFlaming; return this; } public Builder destroyBlocks(boolean dontDestroyBlocks) { this.dontDestroyBlocks = !dontDestroyBlocks; return this; } public VanillaExplosion build() { return new VanillaExplosion(center, strength, dropEverything, isFlaming, dontDestroyBlocks); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/inventory/InventoryManipulation.java ================================================ package net.minestom.vanilla.inventory; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.item.ItemStack; import java.util.Objects; public class InventoryManipulation { public static void consumeItemIfNotCreative(Player player, ItemStack itemStack, PlayerHand hand) { if (player.getGameMode() == GameMode.CREATIVE) { return; } player.setItemInHand(hand, itemStack); } /** * If this function returns false, the operation did not complete. * * @return true if there was enough items to consume, false otherwise. */ public static boolean consumeItemIfNotCreative(Player player, PlayerHand hand, int amount) { if (player.getGameMode() == GameMode.CREATIVE) { return true; } ItemStack item = player.getItemInHand(hand); item = item.withAmount(amt -> amt - amount); if (item.amount() == 0) item = ItemStack.AIR; if (item.amount() < 0) return false; player.setItemInHand(hand, item); return true; } public static void damageItemIfNotCreative(Player player, PlayerHand hand, int amount) { if (player.getGameMode() == GameMode.CREATIVE) { return; } ItemStack itemStack = player.getItemInHand(hand); int damage = Objects.requireNonNullElse(itemStack.get(DataComponents.DAMAGE), 0); int maxDamage = Objects.requireNonNull(itemStack.material().registry().prototype().get(DataComponents.MAX_DAMAGE)); ItemStack newItem = itemStack.with(DataComponents.DAMAGE, damage + amount); if (damage + amount >= maxDamage) { newItem = ItemStack.AIR; // TODO: Item Break Event } player.setItemInHand(hand, newItem); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/Color.java ================================================ package net.minestom.vanilla.logging; public enum Color { //Color end string, color reset RESET("\033[0m"), // Regular Colors. Normal color, no bold, background color etc. BLACK("\033[0;30m"), // BLACK RED("\033[0;31m"), // RED GREEN("\033[0;32m"), // GREEN YELLOW("\033[0;33m"), // YELLOW BLUE("\033[0;34m"), // BLUE MAGENTA("\033[0;35m"), // MAGENTA CYAN("\033[0;36m"), // CYAN WHITE("\033[0;37m"), // WHITE // Bold BLACK_BOLD("\033[1;30m"), // BLACK RED_BOLD("\033[1;31m"), // RED GREEN_BOLD("\033[1;32m"), // GREEN YELLOW_BOLD("\033[1;33m"), // YELLOW BLUE_BOLD("\033[1;34m"), // BLUE MAGENTA_BOLD("\033[1;35m"), // MAGENTA CYAN_BOLD("\033[1;36m"), // CYAN WHITE_BOLD("\033[1;37m"), // WHITE // Underline BLACK_UNDERLINED("\033[4;30m"), // BLACK RED_UNDERLINED("\033[4;31m"), // RED GREEN_UNDERLINED("\033[4;32m"), // GREEN YELLOW_UNDERLINED("\033[4;33m"), // YELLOW BLUE_UNDERLINED("\033[4;34m"), // BLUE MAGENTA_UNDERLINED("\033[4;35m"), // MAGENTA CYAN_UNDERLINED("\033[4;36m"), // CYAN WHITE_UNDERLINED("\033[4;37m"), // WHITE // Background BLACK_BACKGROUND("\033[40m"), // BLACK RED_BACKGROUND("\033[41m"), // RED GREEN_BACKGROUND("\033[42m"), // GREEN YELLOW_BACKGROUND("\033[43m"), // YELLOW BLUE_BACKGROUND("\033[44m"), // BLUE MAGENTA_BACKGROUND("\033[45m"), // MAGENTA CYAN_BACKGROUND("\033[46m"), // CYAN WHITE_BACKGROUND("\033[47m"), // WHITE // High Intensity BLACK_BRIGHT("\033[0;90m"), // BLACK RED_BRIGHT("\033[0;91m"), // RED GREEN_BRIGHT("\033[0;92m"), // GREEN YELLOW_BRIGHT("\033[0;93m"), // YELLOW BLUE_BRIGHT("\033[0;94m"), // BLUE MAGENTA_BRIGHT("\033[0;95m"), // MAGENTA CYAN_BRIGHT("\033[0;96m"), // CYAN WHITE_BRIGHT("\033[0;97m"), // WHITE // Bold High Intensity BLACK_BOLD_BRIGHT("\033[1;90m"), // BLACK RED_BOLD_BRIGHT("\033[1;91m"), // RED GREEN_BOLD_BRIGHT("\033[1;92m"), // GREEN YELLOW_BOLD_BRIGHT("\033[1;93m"), // YELLOW BLUE_BOLD_BRIGHT("\033[1;94m"), // BLUE MAGENTA_BOLD_BRIGHT("\033[1;95m"), // MAGENTA CYAN_BOLD_BRIGHT("\033[1;96m"), // CYAN WHITE_BOLD_BRIGHT("\033[1;97m"), // WHITE // High Intensity backgrounds BLACK_BACKGROUND_BRIGHT("\033[0;100m"), // BLACK RED_BACKGROUND_BRIGHT("\033[0;101m"), // RED GREEN_BACKGROUND_BRIGHT("\033[0;102m"), // GREEN YELLOW_BACKGROUND_BRIGHT("\033[0;103m"), // YELLOW BLUE_BACKGROUND_BRIGHT("\033[0;104m"), // BLUE MAGENTA_BACKGROUND_BRIGHT("\033[0;105m"), // MAGENTA CYAN_BACKGROUND_BRIGHT("\033[0;106m"), // CYAN WHITE_BACKGROUND_BRIGHT("\033[0;107m"); // WHITE private final String code; Color(String code) { this.code = code; } @Override public String toString() { return code; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/Level.java ================================================ package net.minestom.vanilla.logging; public enum Level { TRACE, DEBUG, SETUP, INFO, WARN, ERROR } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/Loading.java ================================================ package net.minestom.vanilla.logging; public interface Loading { static void start(String name) { LoadingImpl.CURRENT.waitTask(name); } static StatusUpdater updater() { return LoadingImpl.CURRENT.getUpdater(); } static void finish() { LoadingImpl.CURRENT.finishTask(); } static void level(Level level) { LoadingImpl.CURRENT.level = level; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/LoadingBar.java ================================================ package net.minestom.vanilla.logging; interface LoadingBar { static LoadingBar console(String initialMessage) { return new LoggingLoadingBar(initialMessage, System.out::print); } static LoadingBar logger(String initialMessage, Logger logger) { return new LoggingLoadingBar(initialMessage, logger::print); } LoadingBar subTask(String task); StatusUpdater updater(); String message(); } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/LoadingImpl.java ================================================ package net.minestom.vanilla.logging; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; class LoadingImpl implements Loading { public static @NotNull LoadingImpl CURRENT = new LoadingImpl(null, null, Level.INFO); private final @Nullable LoadingImpl parent; private final @Nullable LoadingBar loadingBar; private final long started = System.currentTimeMillis(); private final double progress = 0; public Level level; private LoadingImpl(@Nullable LoadingImpl parent, @Nullable LoadingBar loadingBar, Level level) { this.parent = parent; this.loadingBar = loadingBar; this.level = level; } public synchronized void waitTask(String name) { LoadingImpl loading; Logger.logger().level(level).nextLine(); if (loadingBar == null) { loading = new LoadingImpl(this, LoadingBar.logger(name, Logger.logger().level(level)), level); } else { loading = new LoadingImpl(this, loadingBar.subTask(name), level); } CURRENT = loading; } public synchronized void finishTask() { if (loadingBar == null) { throw new IllegalStateException("Cannot finish root task"); } loadingBar.updater().progress(1); assert parent != null; Logger.logger().level(parent.level).printf("took %dms%n", System.currentTimeMillis() - started); CURRENT = this.parent; } public synchronized StatusUpdater getUpdater() { if (loadingBar == null) throw new IllegalStateException("Cannot get updater for root task"); return loadingBar.updater(); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/Logger.java ================================================ package net.minestom.vanilla.logging; public interface Logger { static Logger logger() { return LoggerImpl.DEFAULT; } /** * Debug log entries contain common debug information. */ static Logger debug() { return logger().level(Level.DEBUG); } static Logger debug(String message, Object... args) { if (args.length == 0) return debug().println(message); return debug().printf(message, args).println(); } static Logger debug(Throwable throwable, Object... args) { return debug().throwable(throwable, args); } /** * Setup log entries contain information about the setup of the application. */ static Logger setup() { return logger().level(Level.SETUP); } static Logger setup(String message, Object... args) { if (args.length == 0) return setup().println(message); return setup().printf(message, args); } static Logger setup(Throwable throwable, Object... args) { return setup().throwable(throwable, args); } /** * Info log entries contain important relevant information. */ static Logger info() { return logger().level(Level.INFO); } static Logger info(String message, Object... args) { if (args.length == 0) return info().println(message); return info().printf(message, args).println(); } static Logger info(Throwable throwable, Object... args) { return info().throwable(throwable, args); } /** * Warn log entries contain technical warnings. Typically, warnings do not prevent the application from continuing. */ static Logger warn() { return logger().level(Level.WARN); } static Logger warn(String message, Object... args) { if (args.length == 0) return warn().println(message); return warn().printf(message, args); } static Logger warn(Throwable throwable, Object... args) { return warn().throwable(throwable, args); } /** * Error log entries contain technical errors. Errors WILL stop the application from continuing. */ static Logger error() { return logger().level(Level.ERROR); } static Logger error(String message, Object... args) { if (args.length == 0) return error().println(message); return error().printf(message, args); } static Logger error(Throwable throwable, Object... args) { return error().throwable(throwable, args); } /** * Set the level of the logger * @param level the level * @return the logger */ Logger level(Level level); /** * Gets the current level of the logger */ Level level(); Logger print(String message); default Logger println(String message) { return print(message).println(); } default Logger println() { return print(System.lineSeparator()); } Logger printf(String message, Object... args); Logger throwable(Throwable throwable, Object... args); /** * Ensures that this logger is ready to print to a blank fresh line. * @return the logger */ Logger nextLine(); } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/LoggerImpl.java ================================================ package net.minestom.vanilla.logging; import java.io.OutputStream; import java.io.PrintStream; import java.util.Calendar; import java.util.Objects; import java.util.regex.Pattern; record LoggerImpl(Level level) implements Logger { public static final Level LOG_LEVEL = Level.valueOf(Objects.requireNonNullElse(System.getenv("minestom.vri.log-level"), "SETUP").toUpperCase()); static final LoggerImpl DEFAULT = new LoggerImpl(LOG_LEVEL); private static LoggerImpl lastLogger = DEFAULT; /** true if this logger implementation was the last used to log a message. false if it was an external call to {@link System#out} */ private static boolean loggerWasLast = true; private static boolean newLine = true; private static final Object printLock = new Object(); private static final PrintStream sysOut = System.out; static { System.setOut(new PrintStream(new OutputStream() { @Override public void write(int b) { synchronized (printLock) { if (loggerWasLast) { loggerWasLast = false; if (!newLine) lastLogger.newLine(); } sysOut.write(b); } } }, false)); } @Override public Logger level(Level level) { if (this.level == level) return this; return new LoggerImpl(level); } private void consolePrint(String str) { sysOut.print(str); loggerWasLast = true; lastLogger = this; } private void newLine() { consolePrint(System.lineSeparator()); newLine = true; } @Override public Logger print(String message) { synchronized (printLock) { if (LOG_LEVEL.ordinal() > level.ordinal()) return this; if (loggerWasLast && !lastLogger.equals(this) && !newLine) { newLine(); } if (newLine) { consolePrint(preparePrefix()); newLine = false; } String[] lines = message.split(Pattern.quote(System.lineSeparator()), -1); if (lines.length == 1 && !message.equals(System.lineSeparator())) { printNonNewLine(message); return this; } for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (i != 0) newLine(); if (!line.isEmpty()) print(line); } return this; } } private void printNonNewLine(String message) { if (!message.contains("\r")) { consolePrint(message); return; } String[] split = message.split(Pattern.quote("\r"), -1); consolePrint("\r"); consolePrint(preparePrefix()); consolePrint(split[split.length - 1]); } private String preparePrefix() { Calendar date = Calendar.getInstance(); int seconds = date.get(Calendar.SECOND); int minutes = date.get(Calendar.MINUTE); int hours = date.get(Calendar.HOUR_OF_DAY); int day = date.get(Calendar.DAY_OF_MONTH); int month = date.get(Calendar.MONTH) + 1; int year = date.get(Calendar.YEAR); return String.format("%s[%s/%s/%s %s:%s:%02d]%s %s -> ", Color.GREEN, day, month, year, hours, minutes, seconds, Color.RESET, prepareLevelPrefix()); } @Override public Logger nextLine() { synchronized (printLock) { if (LOG_LEVEL.ordinal() < level.ordinal()) return this; if (!newLine) newLine(); return this; } } private String prepareLevelPrefix() { Color color = switch (level) { case TRACE -> Color.WHITE; case DEBUG -> Color.BLUE; case SETUP -> Color.MAGENTA; case INFO -> Color.CYAN; case WARN -> Color.YELLOW; case ERROR -> Color.RED_BOLD_BRIGHT; }; return String.format("%s(%s)%s", color, level, Color.RESET); } @Override public Logger printf(String message, Object... args) { return print(String.format(message, args)); } @Override public Logger throwable(Throwable throwable, Object... args) { String info = ""; if (args.length == 1) { info = " -> (" + args[0] + ")"; } else if (args.length > 1) { info = " -> (" + String.format(args[0].toString(), args[1]) + ")"; } return print(throwable.getMessage() + info); } @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof LoggerImpl other)) return false; return level == other.level; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/LoggingLoadingBar.java ================================================ package net.minestom.vanilla.logging; import java.util.function.Consumer; class LoggingLoadingBar implements LoadingBar { private static final double PROGRESS_BAR_WIDTH = Integer.parseInt(System.getProperty("vri.loadingBarWidth", "20")); private static final Color MESSAGE_COLOR = Color.valueOf(System.getProperty("vri.loadingBarMessageColor", "BLUE_BOLD")); private String message; private double progress; private final StatusUpdater updater; private final Consumer out; private final int depth = 0; public LoggingLoadingBar(String initialMessage, Consumer out) { this.message = initialMessage; this.progress = 0; this.updater = new UpdaterImpl(); this.out = out; renderThis(); } @Override public SubTaskLoadingBar subTask(String task) { return new SubTaskLoadingBar(this, task, depth + 1); } public StatusUpdater updater() { return updater; } @Override public String message() { return message; } private void renderThis() { out.accept("\r"); render(message, progress, out); } private static void render(String message, double progress, Consumer out) { StringBuilder sb = new StringBuilder(); sb.append(MESSAGE_COLOR); sb.append(message); sb.append(" "); accumulate(progress * PROGRESS_BAR_WIDTH, PROGRESS_BAR_WIDTH, sb); sb.append(" "); out.accept(sb.toString()); } @SuppressWarnings("UnnecessaryUnicodeEscape") private static void accumulate(double width, double total, StringBuilder out) { out.append(Color.RESET); out.append(Color.BLUE_BOLD); out.append("|"); out.append(Color.RESET); out.append(Color.CYAN); double remaining = total - width; while (width > 1) { width -= 1; out.append("="); } out.append(">"); out.append(Color.RESET); out.append(Color.WHITE_UNDERLINED); while (remaining > 1) { remaining -= 1; out.append(" "); } out.append(Color.RESET); out.append(Color.BLUE_BOLD); out.append("|"); out.append(Color.RESET); } private class UpdaterImpl implements StatusUpdater { @Override public synchronized void progress(double progress) { if (LoggingLoadingBar.this.progress != progress) { LoggingLoadingBar.this.progress = progress; renderThis(); } } @Override public synchronized void message(String message) { if (!LoggingLoadingBar.this.message.equals(message)) { LoggingLoadingBar.this.message = message; renderThis(); } } } public class SubTaskLoadingBar implements LoadingBar { private final LoadingBar parent; private String message; private double progress; private final UpdaterImpl updater; private final int depth; public SubTaskLoadingBar(LoadingBar parent, String message, int depth) { this.parent = parent; this.message = message; this.progress = 0; this.updater = new UpdaterImpl(); this.depth = depth; } @Override public LoadingBar subTask(String task) { return new SubTaskLoadingBar(this, task, depth + 1); } private void printIndent(LoadingBar bar) { if (bar instanceof SubTaskLoadingBar sub) { printIndent(sub.parent); } else { out.accept(Color.YELLOW_BRIGHT.toString()); } out.accept("| "); } private void renderThis() { out.accept("\r"); printIndent(parent); out.accept(MESSAGE_COLOR.toString()); render(message, progress, out); } @Override public StatusUpdater updater() { return updater; } @Override public String message() { return message; } private class UpdaterImpl implements StatusUpdater { @Override public void progress(double progress) { if (SubTaskLoadingBar.this.progress != progress) { SubTaskLoadingBar.this.progress = progress; renderThis(); } } @Override public void message(String message) { if (!SubTaskLoadingBar.this.message.equals(message)) { SubTaskLoadingBar.this.message = message; renderThis(); } } } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/SLF4JCompatibilityLayer.java ================================================ package net.minestom.vanilla.logging; import org.slf4j.Marker; import org.slf4j.helpers.AbstractLogger; class SLF4JCompatibilityLayer extends AbstractLogger implements org.slf4j.Logger { private final String name; public SLF4JCompatibilityLayer(String name) { this.name = name; } @Override public String getName() { return "[vri] " + name; } @Override public boolean isTraceEnabled() { return Logger.logger().level().ordinal() <= Level.TRACE.ordinal(); } @Override public boolean isTraceEnabled(Marker marker) { return isTraceEnabled(); } @Override public boolean isDebugEnabled() { return Logger.logger().level().ordinal() <= Level.DEBUG.ordinal(); } @Override public boolean isDebugEnabled(Marker marker) { return isDebugEnabled(); } @Override public boolean isInfoEnabled() { return Logger.logger().level().ordinal() <= Level.INFO.ordinal(); } @Override public boolean isInfoEnabled(Marker marker) { return isInfoEnabled(); } @Override public boolean isWarnEnabled() { return Logger.logger().level().ordinal() <= Level.WARN.ordinal(); } @Override public boolean isWarnEnabled(Marker marker) { return isWarnEnabled(); } @Override public boolean isErrorEnabled() { return Logger.logger().level().ordinal() <= Level.ERROR.ordinal(); } @Override public boolean isErrorEnabled(Marker marker) { return isErrorEnabled(); } @Override protected String getFullyQualifiedCallerName() { return null; } private Level fromSlf4jLevel(org.slf4j.event.Level level) { return switch (level) { case TRACE -> Level.TRACE; case DEBUG -> Level.DEBUG; case INFO -> Level.INFO; case WARN -> Level.WARN; case ERROR -> Level.ERROR; }; } @Override protected void handleNormalizedLoggingCall(org.slf4j.event.Level level, Marker marker, String s, Object[] objects, Throwable throwable) { // TODO: Marker support? Level minestomLevel = fromSlf4jLevel(level); String message = org.slf4j.helpers.MessageFormatter.arrayFormat(s, objects).getMessage(); Logger.logger().level(minestomLevel).println(message); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/SLF4JServiceProvider.java ================================================ package net.minestom.vanilla.logging; import org.slf4j.ILoggerFactory; import org.slf4j.IMarkerFactory; import org.slf4j.helpers.BasicMarkerFactory; import org.slf4j.helpers.NOPMDCAdapter; import org.slf4j.spi.MDCAdapter; public class SLF4JServiceProvider implements org.slf4j.spi.SLF4JServiceProvider { public SLF4JServiceProvider() { } @Override public ILoggerFactory getLoggerFactory() { return SLF4JCompatibilityLayer::new; } private final IMarkerFactory markerFactory = new BasicMarkerFactory(); private final MDCAdapter mdcAdapter = new NOPMDCAdapter(); @Override public IMarkerFactory getMarkerFactory() { return markerFactory; } @Override public MDCAdapter getMDCAdapter() { return mdcAdapter; } @Override public String getRequestedApiVersion() { return "2.0"; } @Override public void initialize() { } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/logging/StatusUpdater.java ================================================ package net.minestom.vanilla.logging; public interface StatusUpdater { /** * Updates the progress bar without changing the text message. * @param progress the progress, between 0 and 1 */ void progress(double progress); /** * Updates the text message without changing the progress bar. * @param message the new message */ void message(String message); } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/EnderChestSystem.java ================================================ package net.minestom.vanilla.system; import net.minestom.server.entity.Player; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; public class EnderChestSystem { public static final EnderChestSystem INSTANCE = new EnderChestSystem(); private final Map> itemsMap = new HashMap<>(); private EnderChestSystem() { } public List getItems(@NotNull Player player) { return getItems(player.getUuid()); } public @NotNull List getItems(@NotNull UUID uuid) { return itemsMap.computeIfAbsent(uuid, k -> List.of()); } public static EnderChestSystem getInstance() { return INSTANCE; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/NetherPortal.java ================================================ package net.minestom.vanilla.system; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.network.packet.server.play.ParticlePacket; import net.minestom.server.network.packet.server.play.WorldEventPacket; import net.minestom.server.particle.Particle; import net.minestom.server.worldevent.WorldEvent; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingDeque; /** * Every useful method linked to Nether portals goes here * TODO: Could be repurposed to create custom portals * * @author jglrxavpok */ @SuppressWarnings("UnstableApiUsage") public final class NetherPortal { private static final int MINIMUM_WIDTH = 2; private static final int MINIMUM_HEIGHT = 3; private static final int MAXIMUM_HEIGHT = 22; private static final int MAXIMUM_WIDTH = 22; private static long nextID = 0; private static final Map portalsById = new HashMap<>(); /** * Only NORTH and WEST are valid */ private final Axis axis; private final Point frameTopLeftCorner; private final Point frameBottomRightCorner; private final Vec averagePosition; private final long id; /** * Prevents considering this portal as non-valid during generation (otherwise portals may try to break themselves when * they are being placed due to neighbor updates of portal blocks) */ private boolean generating; public static final NetherPortal NONE = new NetherPortal(Axis.X, new Pos(0, -1, 0), new Pos(0, -1, 0)); public static @Nullable NetherPortal fromId(@Nullable Long id) { return portalsById.get(id); } public NetherPortal(Axis axis, Point frameBottomRightCorner, Point frameTopLeftCorner) { this.axis = axis; this.frameBottomRightCorner = frameBottomRightCorner; this.frameTopLeftCorner = frameTopLeftCorner; this.averagePosition = new Vec( (frameBottomRightCorner.x() + frameTopLeftCorner.x()) / 2D, (frameBottomRightCorner.y() + frameTopLeftCorner.y()) / 2D, (frameBottomRightCorner.z() + frameTopLeftCorner.z()) / 2D ); this.id = nextID++; portalsById.put(this.id, this); } public Axis getAxis() { return axis; } public Point getFrameBottomRightCorner() { return frameBottomRightCorner; } public Point getFrameTopLeftCorner() { return frameTopLeftCorner; } /** * Position of the center of the frame * Used to compute the closest portal when linking portals */ public Vec getCenter() { return averagePosition; } public boolean isStillValid(Instance instance) { return generating || checkFrameIsObsidian(instance, axis, frameBottomRightCorner, frameTopLeftCorner); } public void breakFrame(Instance instance) { Set blockPositions = new HashSet<>(); replaceFrameContents(instance, true, Block.AIR, blockPositions); for (Point pos : blockPositions) { // play break animation for each portal block int x = pos.blockX(); int y = pos.blockY(); int z = pos.blockZ(); ParticlePacket particlePacket = new ParticlePacket( Particle.BLOCK.withBlock(Block.NETHER_PORTAL), x + 0.5f, y, z + 0.5f, 0.4f, 0.5f, 0.4f, 0.3f, 10 ); WorldEventPacket effectPacket = new WorldEventPacket( WorldEvent.PARTICLES_DESTROY_BLOCK.id(), pos, Block.NETHER_PORTAL.id(), false ); Chunk chunk = instance.getChunkAt(pos); if (chunk != null) { chunk.sendPacketToViewers(particlePacket); chunk.sendPacketToViewers(effectPacket); } } } public boolean tryFillFrame(Instance instance) { if (instance.getDimensionType().key().equals(Key.key("the_end"))) { return false; } // Block to use Block block = Block.NETHER_PORTAL;// .withTag(NetherPortalBlockHandler.RELATED_PORTAL_KEY, this.id()); return replaceFrameContents(instance, true, block, null); } /** * @param instance the instance to build the frame * @param checkPreviousBlocks should check if frame is full of air/portal/fire * @param block the block to place * @param blockPositions the set to fill with block positions */ private boolean replaceFrameContents(Instance instance, boolean checkPreviousBlocks, Block block, @Nullable Set blockPositions) { int minX = Math.min(frameTopLeftCorner.blockX(), frameBottomRightCorner.blockX()); int minY = Math.min(frameBottomRightCorner.blockY(), frameTopLeftCorner.blockY()); int minZ = Math.min(frameTopLeftCorner.blockZ(), frameBottomRightCorner.blockZ()); int maxX = Math.max(frameTopLeftCorner.blockX(), frameBottomRightCorner.blockX()); int maxY = Math.max(frameBottomRightCorner.blockY(), frameTopLeftCorner.blockY()); int maxZ = Math.max(frameTopLeftCorner.blockZ(), frameBottomRightCorner.blockZ()); int width = computeWidth() - 1; // encompasses frame blocks if (checkPreviousBlocks) { if (!checkInsideFrameForAir(instance, minX, maxX, minY, maxY, minZ, maxZ, axis)) { return false; } } // Fill portal int xMul = axis.xMultiplier; int zMul = axis.zMultiplier; for (int d = 1; d < width; d++) { for (int y = minY + 1; y <= maxY - 1; y++) { int x = minX + (d * xMul); int z = minZ + (d * zMul); instance.setBlock(x, y, z, block); if (blockPositions != null) { blockPositions.add(new Pos(x, y, z)); } } } return true; } /** * Gets a {@link NetherPortal} frame description from a block that would be contained inside the frame. * * @param instance the instance to draw blocks from * @param pos the position of the potential future frame block * @return null if no valid frame was found, a new {@link NetherPortal} instance with detailed info otherwise */ public static NetherPortal findPortalFrameFromFrameBlock(Instance instance, Point pos) { NetherPortal alongAxisX = findPortalFrameFromFrameBlock(instance, pos, Axis.X); if (alongAxisX != null) return alongAxisX; return findPortalFrameFromFrameBlock(instance, pos, Axis.Z); } private static NetherPortal findPortalFrameFromFrameBlock(Instance instance, Point frameBlock, Axis axis) { List insideFrame = new LinkedList<>(); List considered = new LinkedList<>(); Queue neighbors = new LinkedBlockingDeque<>(); neighbors.add(frameBlock); while (!neighbors.isEmpty()) { Point position = neighbors.poll(); considered.add(position); int xDistance = Math.abs(position.blockX() - frameBlock.blockX()); int zDistance = Math.abs(position.blockZ() - frameBlock.blockZ()); int height = Math.abs(position.blockY() - frameBlock.blockY()); int width = xDistance * axis.xMultiplier + zDistance * axis.zMultiplier; if (width >= MAXIMUM_WIDTH) { continue; } if (height >= MAXIMUM_HEIGHT) { continue; } Block block = instance.getBlock(position); if (!block.isAir() && block != Block.FIRE && block != Block.NETHER_PORTAL ) { continue; } insideFrame.add(position); Point above = position.add(0, +1, 0); Point below = position.add(0, -1, 0); Point left = position.add(-1 * axis.xMultiplier, 0, -1 * axis.zMultiplier); Point right = position.add(axis.xMultiplier, 0, axis.zMultiplier); if (!considered.contains(above) && !neighbors.contains(above)) { neighbors.add(above); } if (!considered.contains(below) && !neighbors.contains(below)) { neighbors.add(below); } if (!considered.contains(left) && !neighbors.contains(left)) { neighbors.add(left); } if (!considered.contains(right) && !neighbors.contains(right)) { neighbors.add(right); } } // check that insideFrame represent a rectangle full of air int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE; int minZ = Integer.MAX_VALUE; int maxZ = Integer.MIN_VALUE; for (Point framePosition : insideFrame) { int x = framePosition.blockX(); int y = framePosition.blockY(); int z = framePosition.blockZ(); if (x < minX) minX = x; if (y < minY) minY = y; if (z < minZ) minZ = z; if (x > maxX) maxX = x; if (y > maxY) maxY = y; if (z > maxZ) maxZ = z; } boolean isRectangleOfAir = checkInsideFrameForAir(instance, minX, maxX, minY, maxY, minZ, maxZ, axis); if (!isRectangleOfAir) { return null; } int width = (maxX - minX) * axis.xMultiplier + (maxZ - minZ) * axis.zMultiplier + 1; // does not encompass frame int height = maxY - minY + 1; if (width < MINIMUM_WIDTH) { // too narrow return null; } if (height < MINIMUM_HEIGHT) { // too small return null; } Pos bottomRight = null; Pos topLeft = null; switch (axis) { case X -> { bottomRight = new Pos(maxX + 1, minY - 1, minZ); topLeft = new Pos(minX - 1, maxY + 1, minZ); } case Z -> { bottomRight = new Pos(minX, minY - 1, maxZ + 1); topLeft = new Pos(minX, maxY + 1, minZ - 1); } } // TODO: check that frame is obsidian if (!checkFrameIsObsidian(instance, axis, bottomRight, topLeft)) { return null; } return new NetherPortal(axis, bottomRight, topLeft); } private static boolean checkFrameIsObsidian(Instance instance, Axis axis, Point bottomRightCorner, Point topLeftCorner) { int minX = Math.min(topLeftCorner.blockX(), bottomRightCorner.blockX()); int minY = bottomRightCorner.blockY(); int minZ = Math.min(topLeftCorner.blockZ(), bottomRightCorner.blockZ()); int maxX = Math.max(topLeftCorner.blockX(), bottomRightCorner.blockX()); int maxY = topLeftCorner.blockY(); int maxZ = Math.max(topLeftCorner.blockZ(), bottomRightCorner.blockZ()); int width = (maxX - minX) * axis.xMultiplier + (maxZ - minZ) * axis.zMultiplier + 1; // encompasses frame blocks int height = maxY - minY + 1; // offsets by one are used to ignore portal corners // top and bottom for (int i = 1; i < width - 1; i++) { int x = minX; int z = minZ; if (axis == Axis.X) { x += i; } else { z += i; } // bottom Block frameBlock = instance.getBlock(x, minY, z); if (!frameBlock.compare(Block.OBSIDIAN)) { return false; } // top frameBlock = instance.getBlock(x, maxY, z); if (!frameBlock.compare(Block.OBSIDIAN)) { return false; } } // left and right for (int j = 1; j < height - 1; j++) { int x = minX; int z = minZ; // left Block frameBlock = instance.getBlock(x, minY + j, z); if (!frameBlock.compare(Block.OBSIDIAN)) { return false; } if (axis == Axis.X) { x += width - 1; } else { z += width - 1; } // right frameBlock = instance.getBlock(x, minY + j, z); if (!frameBlock.compare(Block.OBSIDIAN)) { return false; } } return true; } private static boolean checkInsideFrameForAir(Instance instance, int minX, int maxX, int minY, int maxY, int minZ, int maxZ, Axis axis) { int width = (maxX - minX) * axis.xMultiplier + (maxZ - minZ) * axis.zMultiplier; for (int i = 1; i <= width - 1; i++) { for (int y = minY + 1; y <= maxY - 1; y++) { int x = minX; int z = minZ; if (axis == Axis.X) { x += i; } else { z += i; } Block currentBlock = instance.getBlock(x, y, z); if (!currentBlock.isAir() && (!currentBlock.compare(Block.FIRE)) && (!currentBlock.compare(Block.NETHER_PORTAL)) ) { return false; } } } return true; } public void unregister(Instance instance) { // instance.setTag(Utils.); // if (instance.getData() != null) { // Data data = instance.getData(); // NetherPortalList list = data.getOrDefault(LIST_KEY, null); // if(list != null) { // list.remove(this); // } // } } public void register(Instance instance) { // if (instance.getData() != null) { // Data data = instance.getData(); // NetherPortalList list = data.getOrDefault(LIST_KEY, null); // if(list == null) { // NetherPortalList newList = new NetherPortalList(); // data.set(LIST_KEY, newList, NetherPortalList.class); // list = newList; // } // // list.add(this); // } } public void generate(Instance instance) { generating = true; // NetherPortalBlockHandler portalBlock = (NetherPortalBlockHandler) Block.NETHER_PORTAL.handler(); loadAround(instance, frameTopLeftCorner).join(); loadAround(instance, frameBottomRightCorner).join(); createFrame(instance); Block block = Block.NETHER_PORTAL; // .withTag(NetherPortalBlockHandler.RELATED_PORTAL_KEY, this.id()); replaceFrameContents(instance, false, block, null); register(instance); generating = false; } /** * Ensure chunks around the portal corner are loaded (3x3 area centered on chunk containing frame corner) */ private CompletableFuture loadAround(Instance instance, Point corner) { int chunkX = corner.blockX() >> 4; int chunkZ = corner.blockZ() >> 4; ObjectArrayList> futures = new ObjectArrayList<>(); for (int x = -1; x <= 1; x++) { for (int z = -1; z <= 1; z++) { futures.add(instance.loadChunk(chunkX + x, chunkZ + z)); } } return CompletableFuture.allOf(futures.elements()); } private void createFrame(Instance instance) { int minX = Math.min(frameTopLeftCorner.blockX(), frameBottomRightCorner.blockX()); int minY = frameBottomRightCorner.blockY(); int minZ = Math.min(frameTopLeftCorner.blockZ(), frameBottomRightCorner.blockZ()); int maxY = frameTopLeftCorner.blockY(); int width = computeWidth(); // encompasses frame blocks int height = computeHeight(); // top and bottom for (int i = 0; i < width; i++) { int x = minX; int z = minZ; if (axis == Axis.X) { x += i; } else { z += i; } // bottom instance.setBlock(x, minY, z, Block.OBSIDIAN); // top instance.setBlock(x, maxY, z, Block.OBSIDIAN); } // left and right for (int j = 0; j < height; j++) { int x = minX; int z = minZ; // left instance.setBlock(x, minY + j, z, Block.OBSIDIAN); if (axis == Axis.X) { x += width - 1; } else { z += width - 1; } // right instance.setBlock(x, minY + j, z, Block.OBSIDIAN); } } public int computeWidth() { int minX = Math.min(frameTopLeftCorner.blockX(), frameBottomRightCorner.blockX()); int minZ = Math.min(frameTopLeftCorner.blockZ(), frameBottomRightCorner.blockZ()); int maxX = Math.max(frameTopLeftCorner.blockX(), frameBottomRightCorner.blockX()); int maxZ = Math.max(frameTopLeftCorner.blockZ(), frameBottomRightCorner.blockZ()); return (maxX - minX) * axis.xMultiplier + (maxZ - minZ) * axis.zMultiplier + 1; } public int computeHeight() { int minY = frameBottomRightCorner.blockY(); int maxY = frameTopLeftCorner.blockY(); return maxY - minY + 1; } public long id() { return this.id; } public enum Axis { X(1, 0), Z(0, 1); public final int xMultiplier; public final int zMultiplier; Axis(int xMultiplier, int zMultiplier) { this.xMultiplier = xMultiplier; this.zMultiplier = zMultiplier; } @Override public String toString() { return name().toLowerCase(); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/RayFastManager.java ================================================ package net.minestom.vanilla.system; import dev.emortal.rayfast.area.area3d.Area3d; import dev.emortal.rayfast.area.area3d.Area3dRectangularPrism; import net.minestom.server.entity.Entity; public class RayFastManager { @SuppressWarnings("UnstableApiUsage") public static void init() { Area3d.CONVERTER.register(Entity.class, entity -> Area3dRectangularPrism.wrapper( entity, ignored -> entity.getBoundingBox().minX() + entity.getPosition().x(), ignored -> entity.getBoundingBox().minY() + entity.getPosition().y(), ignored -> entity.getBoundingBox().minZ() + entity.getPosition().z(), ignored -> entity.getBoundingBox().maxX() + entity.getPosition().x(), ignored -> entity.getBoundingBox().maxY() + entity.getPosition().y(), ignored -> entity.getBoundingBox().maxZ() + entity.getPosition().z() ) ); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/ServerProperties.java ================================================ package net.minestom.vanilla.system; import java.io.*; import java.util.Objects; import java.util.Properties; /** * Helper class to load and save the contents of the server.properties file */ public class ServerProperties { private final File source; private final Properties properties; /** * Creates a new property list from a given file. Will attempt to create the file and fill with defaults if it does not exist */ public ServerProperties(File source) throws IOException { properties = new Properties(); loadDefault(); this.source = source; if (source.exists()) { load(); } else { save(); // write defaults to file } } public ServerProperties(String source) throws IOException { properties = new Properties(); properties.load(new StringReader(source)); this.source = null; } private void loadDefault() throws IOException { try (var defaultInput = new InputStreamReader(Objects.requireNonNull(ServerProperties.class.getResourceAsStream("/server.properties.default")))) { properties.load(defaultInput); } } public void load() throws IOException { try (var reader = new FileReader(source)) { properties.load(reader); } } public String get(String key) { return properties.getProperty(key); } public void set(String key, String value) { properties.put(key, value); } public void save() throws IOException { try (var writer = new FileWriter(source)) { properties.store(writer, "Minestom server properties"); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/nether/EntityEnterNetherPortalEvent.java ================================================ package net.minestom.vanilla.system.nether; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.event.Event; import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.vanilla.system.NetherPortal; import org.jetbrains.annotations.NotNull; /** * Called when a entity starts colliding with a nether portal */ public class EntityEnterNetherPortalEvent implements Event, InstanceEvent, EntityEvent { private final Entity entity; private final Point position; private final NetherPortal portal; public EntityEnterNetherPortalEvent(Entity entity, Point position, NetherPortal portal) { this.entity = entity; this.position = position; this.portal = portal; } @Override public @NotNull Entity getEntity() { return entity; } /** * Position of the portal block which triggered the update * * @return */ public Point getPosition() { return position; } /** * The nether portal the entity is in. Can be null if the portal was added with /setblock * * @return */ public NetherPortal getPortal() { return portal; } @Override public Instance getInstance() { return null; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/nether/NetherPortalTeleportEvent.java ================================================ package net.minestom.vanilla.system.nether; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.event.Event; import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.instance.Instance; import net.minestom.vanilla.system.NetherPortal; import org.jetbrains.annotations.NotNull; /** * Triggered when a nether portal attempts to teleport entities between dimensions */ public class NetherPortalTeleportEvent implements Event, CancellableEvent, EntityEvent { private boolean cancelled = false; private final Entity entity; private final Point portalBlockPosition; private final NetherPortal portal; private final long ticksSpentInPortal; private Instance targetInstance; private Point targetPosition; private NetherPortal targetPortal; private boolean createsNewPortal; public NetherPortalTeleportEvent( Entity entity, Point portalBlockPosition, NetherPortal portal, long ticksSpentInPortal, Instance targetInstance, Point targetPosition, NetherPortal targetPortal, boolean createsNewPortal ) { this.entity = entity; this.portalBlockPosition = portalBlockPosition; this.portal = portal; this.ticksSpentInPortal = ticksSpentInPortal; this.targetInstance = targetInstance; this.targetPosition = targetPosition; this.targetPortal = targetPortal; this.createsNewPortal = createsNewPortal; } /** * Teleporting entity */ public @NotNull Entity getEntity() { return entity; } /** * Position of the portal block which triggered the teleportation */ public Point getPortalBlockPosition() { return portalBlockPosition; } /** * CAN BE NULL. The Nether portal trying to teleport an entity. Can be null if the portal block is not part of a nether portal frame * (for instance, placed with /setblock) */ public NetherPortal getPortal() { return portal; } /** * Number of ticks the entity spent in portal before this event */ public long getTicksSpentInPortal() { return ticksSpentInPortal; } /** * Instance to teleport the entity to */ public Instance getTargetInstance() { return targetInstance; } /** * Instance to teleport the entity to */ public void setTargetDimension(Instance targetInstance) { this.targetInstance = targetInstance; } /** * Position to teleport the entity to. Set to the center of the linked portal, if available */ public Point getTargetPosition() { return targetPosition; } /** * Position to teleport the entity to. Set by default to the center of the linked portal, if available */ public void setTargetPosition(Point targetPosition) { this.targetPosition = targetPosition; } /** * The linked Nether Portal to teleport to, if any */ public NetherPortal getTargetPortal() { return targetPortal; } /** * Set the portal to teleport to. Warning: the position to teleport the entity to is defined by {@link #getTargetPosition()} */ public void setTargetPortal(NetherPortal targetPortal) { this.targetPortal = targetPortal; } /** * Should the teleportation create a new portal on the other side? */ public boolean createsNewPortal() { return createsNewPortal; } /** * @see #createsNewPortal */ public void createsNewPortal(boolean createNewPortal) { this.createsNewPortal = createNewPortal; } @Override public boolean isCancelled() { return cancelled; } @Override public void setCancelled(boolean cancel) { cancelled = cancel; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/system/nether/NetherPortalUpdateEvent.java ================================================ package net.minestom.vanilla.system.nether; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.event.Event; import net.minestom.server.event.trait.EntityEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.vanilla.system.NetherPortal; import org.jetbrains.annotations.NotNull; /** * Called when a nether portal updates the counter inside an entity */ public class NetherPortalUpdateEvent implements Event, EntityEvent, InstanceEvent { private final Entity entity; private final Point position; private final NetherPortal portal; private final Instance instance; private final long tickSpentInPortal; public NetherPortalUpdateEvent(Entity entity, Point position, NetherPortal portal, Instance instance, long tickSpentInPortal) { this.entity = entity; this.position = position; this.portal = portal; this.instance = instance; this.tickSpentInPortal = tickSpentInPortal; } /** * Amount of time spent inside this portal */ public long getTickSpentInPortal() { return tickSpentInPortal; } /** * Position of the portal block which triggered the update */ public Point getPosition() { return position; } /** * The nether portal the entity is in. Can be null if the portal was added with /setblock */ public NetherPortal getPortal() { return portal; } @Override public @NotNull Entity getEntity() { return entity; } @Override public @NotNull Instance getInstance() { return instance; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/tag/Tags.java ================================================ package net.minestom.vanilla.tag; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.*; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.tag.Tag; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; public interface Tags { interface Items { Tag TAG = Tag.NBT("tag") .defaultValue(CompoundBinaryTag.empty()); Tag BLOCKSTATE = TAG.path("BlockEntityTag") .map(nbt -> nbt instanceof CompoundBinaryTag compound ? compound : CompoundBinaryTag.empty(), nbt -> nbt) .defaultValue(CompoundBinaryTag.empty()); interface Banner { record Pattern(String pattern, int color) { } Tag> PATTERNS = BLOCKSTATE.path("Patterns") // TODO: Is this correct? .map(nbt -> new Pattern( nbt.getString("Pattern"), nbt.getInt("Color") ), pattern -> CompoundBinaryTag.from(Map.of( "Pattern", StringBinaryTag.stringBinaryTag(pattern.pattern()), "Color", IntBinaryTag.intBinaryTag(pattern.color()) ))) .list(); } interface Potion { Tag POTION = TAG.path("Potion") .map(nbt -> nbt instanceof StringBinaryTag nbts ? Key.key(nbts.value()) : Key.key("minecraft:empty"), nbt -> StringBinaryTag.stringBinaryTag(nbt.toString())); } } interface Blocks { interface Campfire { Tag> ITEMS = Tag.NBT("Items") .map(nbt -> nbt instanceof CompoundBinaryTag compound ? compound : CompoundBinaryTag.empty(), nbt -> nbt) .list() .map(nbtList -> nbtList.stream() .map(nbt -> ItemStack.of(Material.fromKey(nbt.getString("id")))) .collect(Collectors.toList()), items -> IntStream.range(0, items.size()) .mapToObj(slot -> CompoundBinaryTag.from(Map.of( "id", StringBinaryTag.stringBinaryTag(items.get(slot).material().name()), "Slot", ByteBinaryTag.byteBinaryTag((byte) slot), "Count", ByteBinaryTag.byteBinaryTag((byte) 1) ))) .toList()); // The number of ticks that the campfire has been cooking for, for each of the 4 slots, 0 means end of the cooking Tag> COOKING_PROGRESS = Tag.Integer("vri:campfire:cooking_progress").list().defaultValue(List.of(0, 0, 0, 0)); } interface Smelting { // The number of ticks that the furnace can cook for without using another fuel item Tag COOKING_TICKS = Tag.Integer("vri:furnace:cooking_ticks").defaultValue(0); // The last fuel item that was used. Null if this furnace is not currently burning Tag LAST_COOKED_ITEM = Tag.String("vri:furnace:last_cooked_item") .map(Material::fromKey, mat -> mat.key().toString()) .defaultValue(Material.AIR); // The number of ticks that the furnace has been cooking for Tag COOKING_PROGRESS = Tag.Integer("vri:furnace:cooking_progress").defaultValue(0); } } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/utils/DependencySorting.java ================================================ package net.minestom.vanilla.utils; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class DependencySorting { public interface NamespaceDependent { T identity(); Set dependencies(); } public static > List sort(Set dependants) { List sorted = new ArrayList<>(); Set visited = new HashSet<>(); for (ND dependant : dependants) { visit(dependant, dependants, sorted, visited); } return List.copyOf(sorted); } private static > void visit(ND dependant, Set dependants, List sorted, Set visited) { T identity = dependant.identity(); if (visited.contains(identity)) { return; } visited.add(identity); for (T dependency : dependant.dependencies()) { ND dependencyDependant = dependants.stream() .filter(d -> d.identity().equals(dependency)) .findFirst() .orElseThrow(() -> new IllegalStateException("Missing dependency " + dependency + " for " + identity)); visit(dependencyDependant, dependants, sorted, visited); } sorted.add(dependant); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/utils/JavaUtils.java ================================================ package net.minestom.vanilla.utils; import java.util.Collection; import java.util.random.RandomGenerator; public class JavaUtils { public static T randomElement(RandomGenerator random, Collection collection) { int size = collection.size(); int index = random.nextInt(size); for (T element : collection) { if (index == 0) { return element; } index--; } throw new IllegalStateException("Collection size changed during iteration"); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/utils/MathUtils.java ================================================ package net.minestom.vanilla.utils; import net.minestom.server.coordinate.Point; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; public class MathUtils { public static void forEachWithinManhattanDistance(Point origin, int distance, Consumer consumer) { for (int i = 0; i <= distance; i++) { for (int j = 0; i + j <= distance; j++) { for (int k = 0; i + j + k <= distance; k++) { if (i == 0 && j == 0 && k == 0) { continue; } consumer.accept(origin.add(i, j, k)); if (i != 0) { consumer.accept(origin.add(-i, j, k)); if (j != 0) consumer.accept(origin.add(-i, -j, k)); if (k != 0) { consumer.accept(origin.add(-i, j, -k)); consumer.accept(origin.add(i, j, -k)); consumer.accept(origin.add(-i, -j, -k)); } } if (j != 0) { consumer.accept(origin.add(i, -j, k)); if (k != 0) consumer.accept(origin.add(i, -j, -k)); } if (k != 0) consumer.accept(origin.add(i, j, -k)); } } } } public static Set getWithinManhattanDistance(Point origin, int distance) { Set points = new HashSet<>(); forEachWithinManhattanDistance(origin, distance, points::add); return points; } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/utils/MinestomUtils.java ================================================ package net.minestom.vanilla.utils; import net.minestom.server.MinecraftServer; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.registry.RegistryKey; public class MinestomUtils { /** * Initializes the resources of Minestom. * This is used to not interfere with the timing of initialising the vanilla modules. */ public static void initialize() { Block.values(); Material.values(); EntityType.values(); } public static int getEnchantLevel(ItemStack itemStack, Enchantment enchantment, int defaultValue) { RegistryKey enchant = getEnchantKey(enchantment); if (enchant == null) return defaultValue; EnchantmentList enchants = itemStack.get(DataComponents.ENCHANTMENTS); if (enchants == null) return defaultValue; if (!enchants.has(enchant)) return defaultValue; return enchants.level(enchant); } public static RegistryKey getEnchantKey(Enchantment enchantment) { return MinecraftServer.getEnchantmentRegistry().getKey(enchantment); } } ================================================ FILE: core/src/main/java/net/minestom/vanilla/utils/ZipUtils.java ================================================ package net.minestom.vanilla.utils; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class ZipUtils { private static final Comparator COMPARATOR_STRING_DESC = (str1, str2) -> str1 == null ? 1 : str2 == null ? -1 : -str1.compareTo(str2); public static Map unzip(InputStream is) throws IOException { try (ZipInputStream in = new ZipInputStream(is)) { ZipEntry entry; Map content = new HashMap<>(); Set dirs = new TreeSet<>(COMPARATOR_STRING_DESC); while ((entry = in.getNextEntry()) != null) { String path = removeDirectoryMarker(replaceIncorrectFileSeparators(entry.getName())); if (isDirectory(entry)) { dirs.add(path); } else { content.put(path, in.readAllBytes()); } } addOnlyEmptyDirectories(dirs, content); return content.isEmpty() ? Collections.emptyMap() : content; } } private static boolean isDirectory(ZipEntry entry) { return entry.isDirectory() || entry.getName().endsWith(ILLEGAL_DIR_MARKER); } private static void addOnlyEmptyDirectories(Set dirs, Map content) { if (dirs.isEmpty()) { return; } Set paths = new HashSet<>(content.keySet()); for (String dir : dirs) { boolean empty = true; for (String path : paths) { if (path.startsWith(dir)) { empty = false; break; } } if (empty) { content.put(dir, null); } } } private static final String DIR_MARKER = "/"; private static final String ILLEGAL_DIR_MARKER = "\\"; private static final Pattern BACK_SLASH = Pattern.compile("\\\\"); private static String removeDirectoryMarker(String path) { return path.endsWith(DIR_MARKER) || path.endsWith(ILLEGAL_DIR_MARKER) ? path.substring(0, path.length() - 1) : path; } private static String replaceIncorrectFileSeparators(String path) { return BACK_SLASH.matcher(path).replaceAll(DIR_MARKER); } } ================================================ FILE: core/src/main/resources/META-INF/services/org.tinylog.writers.Writer ================================================ net.minestom.vanilla.VanillaReimplementationWriter ================================================ FILE: core/src/test/java/net/minestom/vanilla/files/FileSystemTests.java ================================================ package net.minestom.vanilla.files; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class FileSystemTests { @Test public void testDynamicWriteRead() { DynamicFileSystem fs = new DynamicFileSystem<>(); assertTrue(fs.folders().isEmpty()); assertTrue(fs.files().isEmpty()); fs.addFile("test.txt", "Hello, world!"); fs.addFile("test2.txt", "Hello, world!"); fs.addFile("test3.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); assertEquals(3, fs.files().size()); assertEquals("Hello, world!", fs.file("test.txt")); assertEquals("Hello, world!", fs.file("test2.txt")); assertEquals("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", fs.file("test3.txt")); fs.folder("testDir"); assertEquals(0, fs.folders().size()); fs.addFolder("testDir").addFile("test4.txt", "Hello, world!"); assertEquals(1, fs.folders().size()); assertEquals(3, fs.files().size()); assertEquals(1, fs.folder("testDir").files().size()); assertEquals("Hello, world!", fs.folder("testDir").file("test4.txt")); } } ================================================ FILE: crafting/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":datapack")) implementation("net.goldenstack:window:${project.property("window_version")}") } ================================================ FILE: crafting/src/main/java/net/minestom/vanilla/crafting/CraftingFeature.java ================================================ package net.minestom.vanilla.crafting; import net.kyori.adventure.key.Key; import net.minestom.server.ServerProcess; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.PlayerBlockInteractEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.vanilla.datapack.Datapacks; import net.minestom.vanilla.logging.Logger; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; import java.util.*; public class CraftingFeature { public record Recipes(@NotNull Crafting crafting, @NotNull Smelting smelting, @NotNull Smithing smithing, @NotNull Map stonecutting, @NotNull Map special) { public Recipes { stonecutting = Map.copyOf(stonecutting); special = Map.copyOf(special); } public static @NotNull Recipes fromRaw(@NotNull Map recipes) { Set> entries = recipes.entrySet(); Recipes parsedRecipes = new Recipes( new Recipes.Crafting( filterRecipes(entries, Recipe.Crafting.Shaped.class), filterRecipes(entries, Recipe.Crafting.Shapeless.class), filterRecipes(entries, Recipe.Crafting.Transmute.class) ), new Recipes.Smelting( filterRecipes(entries, Recipe.Cooking.Smelting.class), filterRecipes(entries, Recipe.Cooking.Smoking.class), filterRecipes(entries, Recipe.Cooking.Blasting.class), filterRecipes(entries, Recipe.Cooking.Campfire.class) ), new Recipes.Smithing( filterRecipes(entries, Recipe.Smithing.Transform.class), filterRecipes(entries, Recipe.Smithing.Trim.class) ), filterRecipes(entries, Recipe.Stonecutting.class), filterRecipes(entries, Recipe.class) // All remaining ); if (!entries.isEmpty()) throw new RuntimeException("Entries should be empty!"); return parsedRecipes; } @SuppressWarnings("unchecked") private static @NotNull Map filterRecipes(@NotNull Iterable> entries, @NotNull Class clazz) { Map map = new HashMap<>(); var iter = entries.iterator(); while (iter.hasNext()) { var entry = iter.next(); if (clazz.isInstance(entry.getValue())) { map.put(entry.getKey(), (T) entry.getValue()); iter.remove(); } } return map; } public record Crafting(@NotNull Map shaped, @NotNull Map shapeless, @NotNull Map transmute) { public Crafting { shaped = Map.copyOf(shaped); shapeless = Map.copyOf(shapeless); transmute = Map.copyOf(transmute); } } public record Smelting(@NotNull Map smelting, @NotNull Map smoking, @NotNull Map blasting, @NotNull Map campfire) { public Smelting { smelting = Map.copyOf(smelting); smoking = Map.copyOf(smoking); blasting = Map.copyOf(blasting); campfire = Map.copyOf(campfire); } } public record Smithing(@NotNull Map transform, @NotNull Map trim) { public Smithing { transform = Map.copyOf(transform); trim = Map.copyOf(trim); } } } public static @NotNull Map buildFromDatapack(@NotNull ServerProcess process) { final Path recipesPath = Path.of("/", "data", "minecraft", "recipe"); Map recipes; try { Path jar = Datapacks.ensureCurrentJarExists(); recipes = Datapacks.buildRegistryFromJar(jar, recipesPath, process, ".json", Recipe.CODEC); } catch (IOException e) { throw new RuntimeException(e); } Logger.info("Loaded and parsed " + recipes.size() + " recipes"); return recipes; } public static @NotNull EventNode createEventNode(@NotNull Map recipeMap, @NotNull ServerProcess process) { // Register recipes recipeMap.values().forEach(process.recipe()::addRecipe); // Parse recipes into usable form Recipes recipes = Recipes.fromRaw(recipeMap); return EventNode.all("vri:recipes") .addChild(new CraftingRecipes(recipes, process).init()) .addListener(PlayerBlockInteractEvent.class, event -> { if (event.getBlock().compare(Block.CRAFTING_TABLE)) event.getPlayer().openInventory(new Inventory(InventoryType.CRAFTING, "Crafting")); }); } } ================================================ FILE: crafting/src/main/java/net/minestom/vanilla/crafting/CraftingRecipes.java ================================================ package net.minestom.vanilla.crafting; import net.goldenstack.window.InventoryView; import net.goldenstack.window.Views; import net.minestom.server.ServerProcess; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.inventory.InventoryCloseEvent; import net.minestom.server.event.inventory.InventoryItemChangeEvent; import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.inventory.AbstractInventory; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.registry.RegistryTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; public record CraftingRecipes(@NotNull CraftingFeature.Recipes recipes, @NotNull ServerProcess process) { public EventNode init() { EventNode node = EventNode.all("vri:recipes-inventory"); node.addListener(InventoryItemChangeEvent.class, addSquareInputSlots( inv -> inv instanceof PlayerInventory, Views.player().crafting().input(), Views.player().crafting().output() )).addListener(InventoryItemChangeEvent.class, addSquareInputSlots( inv -> inv instanceof Inventory crafting && crafting.getInventoryType() == InventoryType.CRAFTING, Views.craftingTable().input(), Views.craftingTable().output() )).addListener(InventoryPreClickEvent.class, addOutputSlot( inv -> inv instanceof PlayerInventory, Views.player().crafting().input(), Views.player().crafting().output() )).addListener(InventoryPreClickEvent.class, addOutputSlot( inv -> inv instanceof Inventory crafting && crafting.getInventoryType() == InventoryType.CRAFTING, Views.craftingTable().input(), Views.craftingTable().output() )); return node; } // Assumes that the input region is square. private @NotNull Consumer addSquareInputSlots(@NotNull Predicate predicate, @NotNull InventoryView input, @NotNull InventoryView.Singular output) { return event -> { final AbstractInventory inv = event.getInventory(); if (!predicate.test(inv)) return; if (!input.isValidExternal(event.getSlot()) && !output.isValidExternal(event.getSlot())) return; int length = (int) Math.round(Math.sqrt(input.size())); Recipe.Crafting recipe = searchRecipe(length, length, input.collect(inv)); output.set(inv, recipe != null ? recipe.result() : ItemStack.AIR); }; } private @NotNull Consumer addOutputSlot(@NotNull Predicate predicate, @NotNull InventoryView input, @NotNull InventoryView.Singular output) { return event -> { final AbstractInventory inv = event.getInventory(); if (!predicate.test(inv)) return; if (!output.isValidExternal(event.getSlot())) return; final ItemStack clicked = output.get(inv); if (clicked.isAir()) { event.setCancelled(true); return; } final PlayerInventory playerInv = event.getPlayer().getInventory(); final ItemStack cursor = playerInv.getCursorItem(); event.setCancelled(true); if (clicked.isSimilar(cursor)) { if (clicked.amount() + cursor.amount() > cursor.maxStackSize()) { return; } playerInv.setCursorItem(cursor.withAmount(cursor.amount() + clicked.amount())); } else if (cursor.isAir()) { playerInv.setCursorItem(clicked); } else { return; } output.set(inv, ItemStack.AIR); for (int i = 0; i < input.size(); i++) { input.set(inv, i, input.get(inv, i).consume(1)); } }; } public @Nullable Recipe.Crafting searchRecipe(int width, int height, @NotNull List ingredients) { final CraftingFeature.Recipes.Crafting crafting = recipes.crafting(); List nonAir = new ArrayList<>(); for (ItemStack item : ingredients) if (!item.isAir()) nonAir.add(item); // Try shapeless for (Recipe.Crafting.Shapeless shapeless : crafting.shapeless().values()) { if (tryShapeless(shapeless, nonAir)) return shapeless; } // Try shaped for (Recipe.Crafting.Shaped shaped : crafting.shaped().values()) { if (tryShaped(shaped, width, height, ingredients)) return shaped; } // Try transmute if (nonAir.size() == 2) { for (Recipe.Crafting.Transmute transmute : crafting.transmute().values()) { if (tryTransmute(transmute, nonAir.getFirst(), nonAir.get(1))) return transmute; } } // TODO: Implement special recipes // for (Recipe recipe : recipes.special().values()) { // // } return null; } public boolean tryShapeless(@NotNull Recipe.Crafting.Shapeless recipe, @NotNull List ingredients) { if (recipe.ingredients().size() != ingredients.size()) return false; // Taking the first valid one strategically has potentially invalid behaviour if we, for example, take an entire // tag instead of a single item, potentially invalidating an otherwise valid recipe had an individual material // been chosen. // We amend this by taking the smallest item first, in terms of the number of options. This is technically not // correct, but should be in enough cases. The correct solution for this is probably some variant of the // knapsack problem, but that might be too much effort for this. // This algorithm should have time complexity O(n^2), but n is always microscopic (n <= 9) so it doesn't really // matter here. boolean[] used = new boolean[ingredients.size()]; // For each material... for (ItemStack item : ingredients) { int indexOfSmallest = -1; // Find the smallest unused tag that fulfills this material for (int index = 0; index < ingredients.size(); index++) { RegistryTag current = recipe.ingredients().get(index); // Only pick ones that fulfill this one and aren't already used if (used[index] || !current.contains(item.material())) continue; if (indexOfSmallest == -1 || current.size() < recipe.ingredients().get(indexOfSmallest).size()) { indexOfSmallest = index; } // Quick exit if smallest if (current.size() == 1) break; } // If there's nothing for this material, invalid. if (indexOfSmallest == -1) return false; // Mark it as true so it isn't reused. used[indexOfSmallest] = true; } return true; } /** * Tries a shaped recipe. Assumes that {@code materials} has a length of {@code width * height}. */ public boolean tryShaped(@NotNull Recipe.Crafting.Shaped shaped, int width, int height, @NotNull List ingredients) { // First, find the size of the recipe inside the grid. int minCol = Integer.MAX_VALUE; int minRow = Integer.MAX_VALUE; int maxCol = Integer.MIN_VALUE; int maxRow = Integer.MIN_VALUE; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { if (ingredients.get(row * height + col).isAir()) continue; if (row < minRow) minRow = row; if (col < minCol) minCol = col; if (row > maxRow) maxRow = row; if (col > maxCol) maxCol = col; } } // There are no items in the grid if anything is still default, but if there's any value at all, it will set all // four values, so we only need to check one. if (minCol == Integer.MAX_VALUE) return false; final int shapeWidth = maxCol - minCol + 1; final int shapeHeight = maxRow - minRow + 1; if (shapeHeight != shaped.pattern().size()) return false; if (shapeWidth != shaped.pattern().getFirst().length()) return false; return tryShapedInPosition(shaped, ingredients, width, height, minRow, minCol); } // Assumes the shaped recipe can fit in the material grid and that nothing has 0 size. private boolean tryShapedInPosition(@NotNull Recipe.Crafting.Shaped shaped, @NotNull List ingredients, int width, int height, int startRow, int startCol) { for (int row = 0; row < shaped.pattern().size(); row++) { final String rowPattern = shaped.pattern().get(row); for (int col = 0; col < rowPattern.length(); col++) { final char charKey = rowPattern.charAt(col); final Material existing = ingredients.get((row + startRow) * height + (col + startCol)).material(); final String key = String.valueOf(charKey); if (!shaped.key().getOrDefault(key, RegistryTag.direct(Material.AIR)).contains(existing)) { return false; } } } return true; } /** * Tries a transmute recipe. */ public boolean tryTransmute(@NotNull Recipe.Crafting.Transmute transmute, @NotNull ItemStack first, @NotNull ItemStack second) { return (transmute.input().contains(first.material()) && transmute.material().contains(second.material())) || (transmute.material().contains(first.material()) && transmute.input().contains(second.material())); } } ================================================ FILE: crafting/src/main/java/net/minestom/vanilla/crafting/Recipe.java ================================================ package net.minestom.vanilla.crafting; import net.kyori.adventure.key.Key; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.recipe.RecipeBookCategory; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.registry.Registries; import net.minestom.server.registry.RegistryTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Map; /** * All recipe types that exist. */ @SuppressWarnings("UnstableApiUsage") public sealed interface Recipe extends net.minestom.server.recipe.Recipe { @NotNull StructCodec CODEC = Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, Recipe::codec, "type"); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("recipes")); // Crafting table recipes registry.register("crafting_shaped", Crafting.Shaped.CODEC); registry.register("crafting_shapeless", Crafting.Shapeless.CODEC); registry.register("crafting_transmute", Crafting.Transmute.CODEC); // Smelting registry.register("smelting", Cooking.Smelting.CODEC); registry.register("smoking", Cooking.Smoking.CODEC); registry.register("blasting", Cooking.Blasting.CODEC); registry.register("campfire_cooking", Cooking.Campfire.CODEC); // Smithing & stonecutting registry.register("smithing_transform", Smithing.Transform.CODEC); registry.register("smithing_trim", Smithing.Trim.CODEC); registry.register("stonecutting", Stonecutting.CODEC); // Special recipes registry.register("crafting_decorated_pot", DecoratedPot.CODEC); registry.register("crafting_special_armordye", SpecialArmorDye.CODEC); registry.register("crafting_special_bannerduplicate", SpecialBannerDuplicate.CODEC); registry.register("crafting_special_bookcloning", SpecialBookCloning.CODEC); registry.register("crafting_special_firework_rocket", SpecialFireworkRocket.CODEC); registry.register("crafting_special_firework_star", SpecialFireworkStar.CODEC); registry.register("crafting_special_firework_star_fade", SpecialFireworkStarFade.CODEC); registry.register("crafting_special_mapcloning", SpecialMapCloning.CODEC); registry.register("crafting_special_mapextending", SpecialMapExtending.CODEC); registry.register("crafting_special_repairitem", SpecialRepairItem.CODEC); registry.register("crafting_special_shielddecoration", SpecialShieldDecoration.CODEC); registry.register("crafting_special_tippedarrow", SpecialTippedArrow.CODEC); return registry; } /** * A crafting recipe - either shaped, shapeless, transmute, or the decorated pot recipe. */ sealed interface Crafting extends Recipe { /** * @return the recipe book category of this recipe */ @NotNull Category category(); /** * @return the item that this recipe produces */ @NotNull ItemStack result(); @Override default @Nullable RecipeBookCategory recipeBookCategory() { return category().category; } enum Category { EQUIPMENT(RecipeBookCategory.CRAFTING_EQUIPMENT), BUILDING(RecipeBookCategory.CRAFTING_BUILDING_BLOCKS), MISC(RecipeBookCategory.CRAFTING_MISC), REDSTONE(RecipeBookCategory.CRAFTING_REDSTONE); private final RecipeBookCategory category; Category(RecipeBookCategory category) { this.category = category; } public static final @NotNull Codec CODEC = Codec.Enum(Category.class); } record Shaped(@Nullable String recipeBookGroup, @NotNull Category category, @NotNull ItemStack result, boolean showNotification, @NotNull List pattern, @NotNull Map> key) implements Crafting { public static final @NotNull StructCodec CODEC = StructCodec.struct( "group", Codec.STRING.optional(), Shaped::recipeBookGroup, "category", Category.CODEC.optional(Category.MISC), Shaped::category, "result", ItemStack.CODEC, Shaped::result, "show_notification", Codec.BOOLEAN.optional(true), Shaped::showNotification, "pattern", Codec.STRING.list(), Shaped::pattern, "key", Codec.STRING.mapValue(RegistryTag.codec(Registries::material)), Shaped::key, Shaped::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } record Shapeless(@Nullable String recipeBookGroup, @NotNull Category category, @NotNull ItemStack result, @NotNull List> ingredients) implements Crafting { public static final @NotNull StructCodec CODEC = StructCodec.struct( "group", Codec.STRING.optional(), Shapeless::recipeBookGroup, "category", Category.CODEC.optional(Category.MISC), Shapeless::category, "result", ItemStack.CODEC, Shapeless::result, "ingredients", RegistryTag.codec(Registries::material).list(), Shapeless::ingredients, Shapeless::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } record Transmute(@Nullable String recipeBookGroup, @NotNull Category category, @NotNull ItemStack result, @NotNull RegistryTag input, @NotNull RegistryTag material) implements Crafting { public static final @NotNull StructCodec CODEC = StructCodec.struct( "group", Codec.STRING.optional(), Transmute::recipeBookGroup, "category", Category.CODEC.optional(Category.MISC), Transmute::category, "result", ItemStack.CODEC, Transmute::result, "input", RegistryTag.codec(Registries::material), Transmute::input, "material", RegistryTag.codec(Registries::material), Transmute::material, Transmute::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } } record Cooking(@Nullable String recipeBookGroup, @Nullable Category category, @NotNull RegistryTag ingredient, @NotNull ItemStack result, int cookingTime, double experience) { public enum Category { FOOD, BLOCKS, MISC; public static final @NotNull Codec CODEC = Codec.Enum(Category.class); } public static @NotNull Codec codec(@Nullable Category defaultCategory) { return StructCodec.struct( "group", Codec.STRING.optional(), Cooking::recipeBookGroup, "category", defaultCategory == null ? Category.CODEC.optional() : Category.CODEC.optional(defaultCategory), Cooking::category, "ingredient", RegistryTag.codec(Registries::material), Cooking::ingredient, "result", ItemStack.CODEC, Cooking::result, "cookingtime", Codec.INT.optional(100), Cooking::cookingTime, "experience", Codec.DOUBLE.optional(0D), Cooking::experience, Cooking::new ); } public record Smelting(@NotNull Cooking cooking) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Cooking.codec(Category.MISC), Smelting::cooking, Smelting::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } public record Smoking(@NotNull Cooking cooking) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Cooking.codec(Category.FOOD), Smoking::cooking, Smoking::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } public record Blasting(@NotNull Cooking cooking) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Cooking.codec(Category.MISC), Blasting::cooking, Blasting::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } public record Campfire(@NotNull Cooking cooking) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Cooking.codec(null), Campfire::cooking, Campfire::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } } record Smithing(@NotNull RegistryTag template, @NotNull RegistryTag base, @NotNull RegistryTag addition) { public static final @NotNull Codec CODEC = StructCodec.struct( "template", RegistryTag.codec(Registries::material), Smithing::template, "base", RegistryTag.codec(Registries::material), Smithing::base, "addition", RegistryTag.codec(Registries::material), Smithing::addition, Smithing::new ); public record Transform(@NotNull Smithing smithing, @NotNull ItemStack result) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Smithing.CODEC, Transform::smithing, "result", ItemStack.CODEC, Transform::result, Transform::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } public record Trim(@NotNull Smithing smithing) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( StructCodec.INLINE, Smithing.CODEC, Trim::smithing, Trim::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } } record Stonecutting(@NotNull RegistryTag ingredient, @NotNull ItemStack result) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( "ingredient", RegistryTag.codec(Registries::material), Stonecutting::ingredient, "result", ItemStack.CODEC, Stonecutting::result, Stonecutting::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } record DecoratedPot(@NotNull Recipe.Crafting.Category category) implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct( "category", Crafting.Category.CODEC.optional(Crafting.Category.MISC), DecoratedPot::category, DecoratedPot::new ); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialArmorDye() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialArmorDye::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialBannerDuplicate() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialBannerDuplicate::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialBookCloning() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialBookCloning::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialFireworkRocket() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialFireworkRocket::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialFireworkStar() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialFireworkStar::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialFireworkStarFade() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialFireworkStarFade::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialMapCloning() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialMapCloning::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialMapExtending() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialMapExtending::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialRepairItem() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialRepairItem::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialShieldDecoration() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialShieldDecoration::new); @Override public @NotNull StructCodec codec() { return CODEC; } } record SpecialTippedArrow() implements Recipe { public static final @NotNull StructCodec CODEC = StructCodec.struct(SpecialTippedArrow::new); @Override public @NotNull StructCodec codec() { return CODEC; } } /** * @return the codec that can encode this recipe */ @NotNull StructCodec codec(); } ================================================ FILE: datapack/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: datapack/src/main/java/net/minestom/vanilla/datapack/Datapacks.java ================================================ package net.minestom.vanilla.datapack; import com.google.gson.JsonParser; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTag; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.codec.Codec; import net.minestom.server.codec.Result; import net.minestom.server.codec.StructCodec; import net.minestom.server.codec.Transcoder; import net.minestom.server.registry.RegistryTranscoder; import net.minestom.vanilla.logging.Loading; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; /** * Utilities for datapack loading. Primary functions are {@link Datapacks#ensureCurrentJarExists()} and {@link * Datapacks#buildRegistryFromJar(Path, Path, ServerProcess, String, Codec)} */ @SuppressWarnings("UnstableApiUsage") public class Datapacks { public static final @NotNull URI VERSION_MANIFEST_URI = URI.create("https://launchermeta.mojang.com/mc/game/version_manifest.json"); public static final @NotNull Path MOJANG_DATA_DIRECTORY = Path.of(".", "mojang-data"); /** * Version manfest data, from {@link Datapacks#VERSION_MANIFEST_URI}. */ public record VersionManifest(@NotNull Latest latest, @NotNull List versions) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "latest", Latest.CODEC, VersionManifest::latest, "versions", Version.CODEC.list(), VersionManifest::versions, VersionManifest::new ); public record Latest(@NotNull String release, @NotNull String snapshot) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "release", Codec.STRING, Latest::release, "snapshot", Codec.STRING, Latest::snapshot, Latest::new ); } public record Version(@NotNull String id, @NotNull Type type, @NotNull String url, @NotNull String time, @NotNull String releaseTime) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "id", Codec.STRING, Version::id, "type", Type.CODEC, Version::type, "url", Codec.STRING, Version::url, "time", Codec.STRING, Version::url, "releaseTime", Codec.STRING, Version::url, Version::new ); public enum Type { RELEASE, SNAPSHOT, OLD_BETA, OLD_ALPHA; public static final @NotNull Codec CODEC = Codec.Enum(Type.class); } } } /** * Version metadata. There's more information on the codec than this, but most of it is not useful here, so it is * not included. */ public record VersionMetadata(@NotNull Downloads downloads) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "downloads", Downloads.CODEC, VersionMetadata::downloads, VersionMetadata::new ); public record Downloads(@NotNull Download client, @Nullable Download clientMappings, @NotNull Download server, @Nullable Download serverMappings) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "client", Download.CODEC, Downloads::client, "client_mappings", Download.CODEC.optional(), Downloads::clientMappings, "server", Download.CODEC, Downloads::server, "server_mappings", Download.CODEC.optional(), Downloads::serverMappings, Downloads::new ); } public record Download(@NotNull String sha1, int size, @NotNull String url) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "sha1", Codec.STRING, Download::sha1, "size", Codec.INT, Download::size, "url", Codec.STRING, Download::url, Download::new ); } } /** * Extracts various metadata (including JAR URLs) about the specified version. Use "latest" for the latest release. */ public static @NotNull URL getVersionMetadataURL(@NotNull String version) throws IOException { URL discoveryUrl = VERSION_MANIFEST_URI.toURL(); VersionManifest manifest; try (InputStream source = discoveryUrl.openStream(); InputStreamReader reader = new InputStreamReader(source)) { Result result = VersionManifest.CODEC.decode(Transcoder.JSON, JsonParser.parseReader(reader)); manifest = result.orElseThrow("failed to parse version manifest at url " + VERSION_MANIFEST_URI); } if (version.equals("latest")) { version = manifest.latest().release(); } for (VersionManifest.Version versionEntry : manifest.versions()) { if (versionEntry.id().equals(version)) { return URI.create(versionEntry.url()).toURL(); } } throw new IllegalArgumentException("Version '" + version + "' not found!"); } /** * Extracts the client JAR url from the version metadata. */ public static @NotNull URL getClientJarURL(@NotNull URL versionMetadata) throws IOException { VersionMetadata metadata; try (InputStream source = versionMetadata.openStream(); InputStreamReader reader = new InputStreamReader(source)) { Result result = VersionMetadata.CODEC.decode(Transcoder.JSON, JsonParser.parseReader(reader)); metadata = result.orElseThrow("failed to parse version metadata at url " + versionMetadata); } String url = metadata.downloads().client().url(); return URI.create(url).toURL(); } /** * Downloads the vanilla JAR from a specified source URL to the given sink file. Other than log messages, this is * entirely agnostic of the actual file content and simply performs a copy while logging. */ public static void downloadJar(@NotNull URL sourceUrl, @NotNull Path sinkFile) throws IOException { URLConnection sourceConnection = sourceUrl.openConnection(); sourceConnection.connect(); final int parts = 8; int len = sourceConnection.getContentLength(); double totalMB = (double) len / 1024 / 1024; Loading.start(String.format("Downloading vanilla jar (%.2f MB)...", totalMB)); byte[] buf = new byte[4096]; // Ensure necessary parent directories for the JAR exist Files.createDirectories(sinkFile.getParent()); try (InputStream source = sourceConnection.getInputStream(); OutputStream sink = Files.newOutputStream(sinkFile)) { for (int cur = 0, read; (read = source.read(buf)) > 0; cur += read) { sink.write(buf, 0, read); // If it passed a boundary (wraps around mod len/parts after reading), display progress if ((cur + read) % (len/parts) < cur % (len/parts)) { double progress = cur / (double) len; Loading.updater().progress(Math.round(progress * parts) / (double) parts); } } } Loading.finish(); } /** * Downloads the client JAR for the specified version and places it in {@code client-VERSION.jar} within the * specified directory. No-op of the client JAR has been downloaded already. * @return the client JAR path */ public static @NotNull Path discoverAndDownloadJar(@NotNull String version, @NotNull Path sinkDirectory) throws IOException { Path sinkFile = sinkDirectory.resolve("client-" + version + ".jar"); if (Files.exists(sinkFile)) return sinkFile; // Cached URL jarUrl = Datapacks.getClientJarURL(Datapacks.getVersionMetadataURL(version)); Datapacks.downloadJar(jarUrl, sinkFile); return sinkFile; } /** * Ensures that the client JAR exists and is downloaded, returning its path. */ public static @NotNull Path ensureCurrentJarExists() throws IOException { return Datapacks.discoverAndDownloadJar(MinecraftServer.VERSION_NAME, MOJANG_DATA_DIRECTORY); } @SuppressWarnings("PatternValidation") public static @NotNull Map buildRegistryFromJar(@NotNull Path jarPath, @NotNull Path pathFilter, @NotNull ServerProcess process, @NotNull String fileSuffix, @NotNull Codec codec) throws IOException { final Map map = new HashMap<>(); final Transcoder coder = new RegistryTranscoder<>(Transcoder.NBT, process); try (FileSystem fileSystem = FileSystems.newFileSystem(jarPath)) { for (Path root : fileSystem.getRootDirectories()) { Path relevantFiles = root.resolve(pathFilter.toString()); // Prevent provider mismatch try (Stream paths = Files.walk(relevantFiles)) { for (Path path : paths.toList()) { if (!Files.isRegularFile(path)) continue; if (!path.toString().endsWith(".json")) continue; String keyPath = relevantFiles.relativize(path).toString(); keyPath = keyPath.substring(0, keyPath.length() - fileSuffix.length()); BinaryTag tag; try { tag = MinestomAdventure.tagStringIO().asTag(Files.readString(path)); } catch (IOException e) { throw new RuntimeException(e); } map.put( Key.key(keyPath), codec.decode(coder, tag).orElseThrow("parsing " + path) ); } } } } return map; } } ================================================ FILE: datapack-loading/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":mojang-data")) implementation("space.vectrix.flare:flare:2.0.1") implementation("space.vectrix.flare:flare-fastutil:2.0.1") } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/Datapack.java ================================================ package net.minestom.vanilla.datapack; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.advancement.Advancement; import net.minestom.vanilla.datapack.dimension.DimensionType; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import net.minestom.vanilla.datapack.loot.LootTable; import net.minestom.vanilla.datapack.loot.function.LootFunction; import net.minestom.vanilla.datapack.loot.function.Predicate; import net.minestom.vanilla.datapack.recipe.Recipe; import net.minestom.vanilla.datapack.trims.TrimMaterial; import net.minestom.vanilla.datapack.trims.TrimPattern; import net.minestom.vanilla.datapack.worldgen.*; import net.minestom.vanilla.datapack.worldgen.noise.Noise; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; public interface Datapack { Map namespacedData(); static Datapack loadPrimitiveByteArray(FileSystem source) { return loadByteArray(source.map(ByteArray::wrap)); } static Datapack loadInputStream(FileSystem source) { return loadPrimitiveByteArray(source.map(stream -> { try { return stream.readAllBytes(); } catch (IOException e) { throw new RuntimeException(e); } })); } static Datapack loadByteArray(FileSystem source) { return new DatapackLoader().load(source.cache()); } record McMeta(Pack pack, Filter filter) { public McMeta() { // Default this(new Pack(6, "Minestom Vanilla Datapack"), new Filter(List.of())); } record Pack(int pack_format, String description) { } record Filter(List block) { record Pattern(String namespace, String path) { } } } record NamespacedData(FileSystem advancements, FileSystem functions, FileSystem item_modifiers, FileSystem loot_tables, FileSystem predicates, FileSystem recipes, FileSystem structures, FileSystem chat_type, FileSystem damage_type, FileSystem tags, FileSystem dimensions, FileSystem dimension_type, FileSystem trim_pattern, FileSystem trim_material, WorldGen world_gen) { /** * Performs a deep cache on all of the data. * This helps us load all density functions while in the loading context. */ NamespacedData cache() { return new NamespacedData( advancements.cache(), functions.cache(), item_modifiers.cache(), loot_tables.cache(), predicates.cache(), recipes.cache(), structures.lazy(), // structures may be large, so we don't want to cache them immediately chat_type.cache(), damage_type.cache(), tags.cache(), dimensions.cache(), dimension_type.cache(), trim_pattern.cache(), trim_material.cache(), world_gen.cache() ); } } record McFunction(String source) { public static McFunction fromString(String source) { return new McFunction(source); } } record ChatType() { // Undocumented? Couldn't find any info on this } record DamageType(String message_id, float exhaustion, String scaling, @Nullable String effects, @Nullable String death_message_type) { } record Tag(@Nullable Boolean replace, List values) { public Tag { values = List.copyOf(values); } public sealed interface TagValue { static TagValue fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> DatapackLoader.moshi(ObjectOrTagReference.class); case BEGIN_OBJECT -> DatapackLoader.moshi(TagEntry.class); default -> null; }); } record ObjectOrTagReference(Key tag) implements TagValue { public static ObjectOrTagReference fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.STRING, json -> new ObjectOrTagReference(DatapackLoader.jsonAdaptor(Key.class).fromJson(json)) )); } } /** * @param required defaults to true */ record TagEntry(ObjectOrTagReference id, @Optional Boolean required) implements TagValue { } } } record Dimension(Key type) { } record WorldGen( FileSystem biome, FileSystem configured_carver, FileSystem configured_feature, FileSystem density_function, FileSystem flat_level_generator_preset, FileSystem multi_noise_biome_source_parameter_list, FileSystem noise, FileSystem noise_settings, FileSystem placed_feature, FileSystem processor_list, FileSystem structure, FileSystem structure_set, FileSystem template_pool, FileSystem world_preset ) { public static WorldGen from(FileSystem worldgen) { return new WorldGen( DatapackLoader.parseJsonFolder(worldgen, "biome", DatapackLoader.adaptor(Biome.class)), DatapackLoader.parseJsonFolder(worldgen, "configured_carver", DatapackLoader.adaptor(Carver.class)), worldgen.folder("configured_feature"), DatapackLoader.parseJsonFolder(worldgen, "density_function", DatapackLoader.adaptor(DensityFunction.class)), worldgen.folder("flat_level_generator_preset"), worldgen.folder("multi_noise_biome_source_parameter_list"), DatapackLoader.parseJsonFolder(worldgen, "noise", DatapackLoader.adaptor(Noise.class)), DatapackLoader.parseJsonFolder(worldgen, "noise_settings", DatapackLoader.adaptor(NoiseSettings.class)), worldgen.folder("placed_feature"), worldgen.folder("processor_list"), worldgen.folder("structure"), worldgen.folder("structure_set"), worldgen.folder("template_pool"), worldgen.folder("world_preset") ); } public WorldGen cache() { return new WorldGen( biome.cache(), configured_carver.cache(), configured_feature.cache(), density_function.cache(), flat_level_generator_preset.cache(), multi_noise_biome_source_parameter_list.cache(), noise.cache(), noise_settings.cache(), placed_feature.cache(), processor_list.cache(), structure.cache(), structure_set.cache(), template_pool.cache(), world_preset.cache() ); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/DatapackLoader.java ================================================ package net.minestom.vanilla.datapack; import com.squareup.moshi.*; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; import it.unimi.dsi.fastutil.doubles.DoubleList; import it.unimi.dsi.fastutil.doubles.DoubleLists; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.TagStringIO; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.utils.Range; import net.minestom.vanilla.datapack.advancement.Advancement; import net.minestom.vanilla.datapack.dimension.DimensionType; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.loot.LootTable; import net.minestom.vanilla.datapack.loot.NBTPath; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.datapack.loot.function.LootFunction; import net.minestom.vanilla.datapack.loot.function.Predicate; import net.minestom.vanilla.datapack.number.NumberProvider; import net.minestom.vanilla.datapack.recipe.Recipe; import net.minestom.vanilla.datapack.tags.Tag; import net.minestom.vanilla.datapack.trims.TrimMaterial; import net.minestom.vanilla.datapack.trims.TrimPattern; import net.minestom.vanilla.datapack.worldgen.*; import net.minestom.vanilla.datapack.worldgen.math.CubicSpline; import net.minestom.vanilla.datapack.worldgen.noise.Noise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.lang.reflect.Type; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import static net.minestom.vanilla.datapack.Datapack.*; public class DatapackLoader { private static final Moshi moshi = createMoshiWithAdaptors(); DatapackLoader() { } private static Moshi createMoshiWithAdaptors() { Moshi.Builder builder = new Moshi.Builder(); // Native register(builder, UUID.class, DatapackLoader::uuidFromJson); // json utils builder.add((type, annotations, moshi) -> { if (typeDoesntMatch(type, JsonUtils.SingleOrList.class)) return null; Type elementType = Types.collectionElementType(type, Collection.class); return new JsonAdapter>() { @Override public JsonUtils.SingleOrList fromJson(@NotNull JsonReader reader) throws IOException { return JsonUtils.SingleOrList.fromJson(elementType, reader); } @Override public void toJson(@NotNull JsonWriter writer, JsonUtils.SingleOrList value) { } }; }); // Minestom register(builder, CompoundBinaryTag.class, DatapackLoader::nbtCompoundFromJson); register(builder, Block.class, DatapackLoader::blockFromJson); register(builder, Enchantment.class, DatapackLoader::enchantmentFromJson); register(builder, EntityType.class, DatapackLoader::entityTypeFromJson); register(builder, Material.class, DatapackLoader::materialFromJson); register(builder, Component.class, reader -> { GsonComponentSerializer serializer = GsonComponentSerializer.gson(); return serializer.deserialize(reader.nextSource().readUtf8()); }); register(builder, Key.class, reader -> { String str = reader.nextString(); return str.startsWith("#") ? new Tag(str.substring(1)) : Key.key(str); }); register(builder, Range.Float.class, DatapackLoader::floatRangeFromJson); // Misc register(builder, DoubleList.class, DatapackLoader::doubleListFromJson); // VRI Datapack register(builder, Advancement.Trigger.class, Advancement.Trigger::fromJson); register(builder, BlockState.class, BlockState::fromJson); register(builder, LootContext.Trait.class, LootContext.Trait::fromJson); register(builder, LootFunction.class, LootFunction::fromJson); register(builder, Predicate.class, Predicate::fromJson); register(builder, Predicate.BlockStateProperty.Property.class, Predicate.BlockStateProperty.Property::fromJson); register(builder, Predicate.EntityScores.Score.class, Predicate.EntityScores.Score::fromJson); register(builder, Predicate.TimeCheck.Value.class, Predicate.TimeCheck.Value::fromJson); register(builder, Predicate.ValueCheck.Range.class, Predicate.ValueCheck.Range::fromJson); register(builder, NumberProvider.class, NumberProvider.Double::fromJson); register(builder, NumberProvider.Double.class, NumberProvider.Double::fromJson); register(builder, NumberProvider.Int.class, NumberProvider.Int::fromJson); register(builder, LootTable.Pool.Entry.class, LootTable.Pool.Entry::fromJson); register(builder, LootFunction.ApplyBonus.class, LootFunction.ApplyBonus::fromJson); register(builder, LootFunction.CopyNbt.Source.class, LootFunction.CopyNbt.Source::fromJson); register(builder, LootFunction.CopyNbt.Operation.class, LootFunction.CopyNbt.Operation::fromJson); register(builder, LootFunction.LimitCount.Limit.class, LootFunction.LimitCount.Limit::fromJson); register(builder, Recipe.class, Recipe::fromJson); register(builder, Recipe.Ingredient.class, Recipe.Ingredient::fromJson); register(builder, Recipe.Ingredient.Single.class, Recipe.Ingredient.Single::fromJson); register(builder, NBTPath.class, NBTPath::fromJson); register(builder, NBTPath.Single.class, NBTPath.Single::fromJson); register(builder, DensityFunction.class, DensityFunction::fromJson); register(builder, Noise.class, Noise::fromJson); register(builder, NoiseSettings.SurfaceRule.class, NoiseSettings.SurfaceRule::fromJson); register(builder, NoiseSettings.SurfaceRule.SurfaceRuleCondition.class, NoiseSettings.SurfaceRule.SurfaceRuleCondition::fromJson); register(builder, VerticalAnchor.class, VerticalAnchor::fromJson); register(builder, CubicSpline.class, CubicSpline::fromJson); register(builder, DensityFunction.OldBlendedNoise.class, DensityFunction.OldBlendedNoise::fromJson); register(builder, Datapack.Tag.TagValue.class, Datapack.Tag.TagValue::fromJson); register(builder, Datapack.Tag.TagValue.ObjectOrTagReference.class, Datapack.Tag.TagValue.ObjectOrTagReference::fromJson); register(builder, Biome.Effects.Particle.Options.class, Biome.Effects.Particle.Options::fromJson); register(builder, Biome.Sound.class, Biome.Sound::fromJson); register(builder, Carver.class, Carver::fromJson); register(builder, FloatProvider.class, FloatProvider::fromJson); register(builder, Biome.CarversList.class, Biome.CarversList::fromJson); register(builder, Biome.CarversList.Single.class, Biome.CarversList.Single::fromJson); register(builder, HeightProvider.class, HeightProvider::fromJson); return builder.build(); } static FileSystem parseJsonFolder(FileSystem source, String path, Function converter) { return source.folder(path).map(FileSystem.BYTES_TO_STRING).map(converter); } public static Function adaptor(Class clazz) { return str -> { try { return jsonAdaptor(clazz).fromJson(str); } catch (IOException e) { throw new RuntimeException(e); } }; } public static JsonAdapter jsonAdaptor(Class clazz) { return moshi.adapter(clazz); } private static final ThreadLocal contextPool = new ThreadLocal<>(); public static LoadingContext loading() { LoadingContext context = contextPool.get(); if (context == null) { return STATIC_CONTEXT; } return context; } public interface LoadingContext { WorldgenRandom random(); void whenFinished(Consumer finishAction); default boolean isStatic() { return false; } } private static final LoadingContext STATIC_CONTEXT = new LoadingContext() { @Override public WorldgenRandom random() { return WorldgenRandom.xoroshiro(0); } @Override public void whenFinished(Consumer finishAction) { throw new RuntimeException(new IllegalAccessException("Not in a datapack loading context")); } @Override public boolean isStatic() { return true; } }; public interface DatapackFinisher { Datapack datapack(); } public Datapack load(FileSystem source) { // Default McMeta mcmeta; mcmeta = !source.hasFile("pack.mcmeta") ? new McMeta() : source.map(FileSystem.BYTES_TO_STRING).map(adaptor(McMeta.class)).file("pack.mcmeta"); @Nullable ByteArray pack_png = !source.hasFile("pack.png") ? null : source.file("pack.png"); // ImageIO.read(pack_png.toStream()); // Load this datapack on this thread, so that we can use the thread-local contextPool WorldgenRandom loading = WorldgenRandom.xoroshiro(0); Queue> finishers = new ArrayDeque<>(); LoadingContext context = new LoadingContext() { @Override public WorldgenRandom random() { return loading; } @Override public void whenFinished(Consumer finishAction) { finishers.add(finishAction); } }; contextPool.set(context); Map namespace2data; { namespace2data = new HashMap<>(); for (String namespace : source.folders()) { FileSystem dataFolder = source.folder(namespace).inMemory(); FileSystem advancements = parseJsonFolder(dataFolder, "advancement", adaptor(Advancement.class)); FileSystem functions = parseJsonFolder(dataFolder, "functions", McFunction::fromString); FileSystem item_modifiers = parseJsonFolder(dataFolder, "item_modifiers", adaptor(LootFunction.class)); FileSystem loot_tables = parseJsonFolder(dataFolder, "loot_tables", adaptor(LootTable.class)); FileSystem predicates = parseJsonFolder(dataFolder, "predicates", adaptor(Predicate.class)); FileSystem recipes = parseJsonFolder(dataFolder, "recipe", adaptor(Recipe.class)); FileSystem structures = dataFolder.folder("structures").map(Structure::fromInput); FileSystem chat_type = parseJsonFolder(dataFolder, "chat_type", adaptor(ChatType.class)); FileSystem damage_type = parseJsonFolder(dataFolder, "damage_type", adaptor(DamageType.class)); FileSystem tags = parseJsonFolder(dataFolder, "tags", adaptor(Datapack.Tag.class)); FileSystem dimensions = parseJsonFolder(dataFolder, "dimension", adaptor(Dimension.class)); FileSystem dimension_type = parseJsonFolder(dataFolder, "dimension_type", adaptor(DimensionType.class)); FileSystem trim_pattern = parseJsonFolder(dataFolder, "trim_pattern", adaptor(TrimPattern.class)); FileSystem trim_material = parseJsonFolder(dataFolder, "trim_material", adaptor(TrimMaterial.class)); Datapack.WorldGen world_gen = Datapack.WorldGen.from(dataFolder.folder("worldgen")); NamespacedData data = new NamespacedData(advancements, functions, item_modifiers, loot_tables, predicates, recipes, structures, chat_type, damage_type, tags, dimensions, dimension_type, trim_pattern, trim_material, world_gen); namespace2data.put(namespace, data); } } var copy = namespace2data.entrySet().stream() .map(entry -> Map.entry(entry.getKey(), entry.getValue().cache())) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); Datapack datapack = new Datapack() { @Override public Map namespacedData() { return copy; } @Override public String toString() { return "Datapack{" + "namespace2data=" + copy + '}'; } }; // new we can finish the datapack while (!finishers.isEmpty()) { finishers.poll().accept(() -> datapack); } contextPool.remove(); return datapack; } private static void register(Moshi.Builder builder, Class clazz, JsonAdapter adapter) { builder.add((type, annotations, moshi) -> { if (typeDoesntMatch(type, clazz)) return null; return adapter; }); } private static void register(Moshi.Builder builder, Class clazz, IoFunction reader) { register(builder, clazz, new IoJsonAdaptor<>(reader)); } private static class IoJsonAdaptor extends JsonAdapter { private final IoFunction reader; public IoJsonAdaptor(IoFunction reader) { this.reader = reader; } @Override public T fromJson(JsonReader jsonReader) throws IOException { return reader.apply(jsonReader); } @Override public void toJson(JsonWriter writer, T value) throws IOException { } } public interface IoFunction { R apply(T t) throws IOException; } public static Moshi moshi() { return moshi; } public static JsonUtils.IoFunction moshi(Class clazz) { return moshi().adapter(clazz)::fromJson; } public static JsonUtils.IoFunction moshi(Type type) { JsonUtils.IoFunction function = moshi().adapter(type)::fromJson; //noinspection unchecked return (JsonUtils.IoFunction) function; } private static CompoundBinaryTag nbtCompoundFromJson(JsonReader reader) throws IOException { String json = reader.nextSource().readUtf8(); return TagStringIO.get().asCompound(json); } private static Key keyFromJson(JsonReader reader) throws IOException { return Key.key(reader.nextString()); } private static UUID uuidFromJson(JsonReader reader) throws IOException { throw new UnsupportedOperationException("UUIDs are not supported yet"); } private static Block blockFromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.STRING, json -> Block.fromKey(json.nextString()), JsonReader.Token.BEGIN_OBJECT, json -> DatapackLoader.moshi(BlockState.class).apply(json).toMinestom() )); } private static Enchantment enchantmentFromJson(JsonReader reader) throws IOException { return MinecraftServer.getEnchantmentRegistry().get(Key.key(reader.nextString())); } private static EntityType entityTypeFromJson(JsonReader reader) throws IOException { return EntityType.fromKey(keyFromJson(reader)); } private static Material materialFromJson(JsonReader reader) throws IOException { Key key = keyFromJson(reader); Material mat = Material.fromKey(key); // TODO: Remove these legacy updates Map legacy = Map.of( Key.key("scute"), Material.TURTLE_SCUTE ); if (mat == null) { if (legacy.containsKey(key)) { return legacy.get(key); } throw new IllegalStateException("Material not found: " + key); } return mat; } private static Range.Float floatRangeFromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case BEGIN_ARRAY -> json -> { json.beginArray(); var range = new Range.Float((float) json.nextDouble(), (float) json.nextDouble()); json.endArray(); return range; }; case NUMBER -> json -> new Range.Float((float) json.nextDouble()); default -> null; }); } private static DoubleList doubleListFromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case BEGIN_ARRAY -> json -> { json.beginArray(); DoubleList list = new DoubleArrayList(); while (json.peek() == JsonReader.Token.NUMBER) { list.add(json.nextDouble()); } json.endArray(); return DoubleLists.unmodifiable(list); }; default -> null; }); } private static final Pattern GENERICS = Pattern.compile("<.*>"); private static boolean typeDoesntMatch(Type type, Class clazz) { String typeName = GENERICS.matcher(type.getTypeName()).replaceAll(""); String clazzName = GENERICS.matcher(clazz.getTypeName()).replaceAll(""); return !typeName.equals(clazzName); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/DatapackLoadingFeature.java ================================================ package net.minestom.vanilla.datapack; import io.github.pesto.MojangDataFeature; import net.kyori.adventure.key.Key; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import net.minestom.vanilla.logging.Loading; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnknownNullability; import java.util.Objects; import java.util.Set; public class DatapackLoadingFeature implements VanillaReimplementation.Feature { private @UnknownNullability Datapack datapack; @Override public void hook(@NotNull HookContext context) { @NotNull MojangDataFeature data = context.vri().feature(MojangDataFeature.class); Loading.start("Parsing vanilla datapack"); FileSystem fs = data.latestAssets(); datapack = Datapack.loadByteArray(fs); Loading.finish(); } public @NotNull Datapack current() { Objects.requireNonNull(datapack, "DatapackLoadingFeature not loaded yet"); return datapack; } @Override public @NotNull Key key() { return Key.key("vri:datapack"); } @Override public @NotNull Set> dependencies() { return Set.of(MojangDataFeature.class); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/DatapackUtils.java ================================================ package net.minestom.vanilla.datapack; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.tags.Tag; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.noise.Noise; import net.minestom.vanilla.files.FileSystem; import org.jetbrains.annotations.Nullable; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; public class DatapackUtils { public static Optional findNoise(Datapack datapack, String file) { return findInJsonData(file, datapack, data -> data.world_gen().noise()); } public static Optional findDensityFunction(Datapack datapack, String file) { return findInJsonData(file, datapack, data -> data.world_gen().density_function()); } /** Finds the tag items for the given tag type and namespace ID */ public static Set findTags(Datapack datapack, String tagType, Key namespaceID) { Datapack.NamespacedData data = datapack.namespacedData().get(namespaceID.namespace()); if (data == null) return Set.of(); var itemTags = data.tags().folder(tagType); var itemTag = itemTags.file(namespaceID.value() + ".json"); if (itemTag == null) return Set.of(); return resolveTagItems(datapack, itemTag); } private static Set resolveTagItems(Datapack datapack, Datapack.Tag tag) { Set materials = new HashSet<>(); for (Datapack.Tag.TagValue value : tag.values()) { resolveTagValue(datapack, value, materials::add); } return Set.copyOf(materials); } private static void resolveTagValue(Datapack datapack, Datapack.Tag.TagValue value, Consumer out) { if (value instanceof Datapack.Tag.TagValue.ObjectOrTagReference objectOrTagReference) { if (objectOrTagReference.tag() instanceof Tag tag) { var mats = resolveReferenceTag(datapack, tag); if (mats != null) { mats.forEach(out); return; } throw new UnsupportedOperationException("Unable to resolve where tag " + tag + " is pointing to"); } // found the material out.accept(objectOrTagReference.tag()); return; } if (value instanceof Datapack.Tag.TagValue.TagEntry tagEntry) { try { resolveTagValue(datapack, tagEntry.id(), out); } catch (UnsupportedOperationException e) { if (tagEntry.required() == null || tagEntry.required()) { throw e; } } return; } throw new UnsupportedOperationException("Unknown tag value type " + value.getClass().getName()); } private static @Nullable Set resolveReferenceTag(Datapack datapack, Key tagNamespace) { // otherwise resolve to another tag for (var entry : datapack.namespacedData().entrySet()) { String namespace = entry.getKey(); Datapack.NamespacedData data = entry.getValue(); var itemTags = data.tags().folder("item"); for (var itemEntry : itemTags.files().stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), itemTags::file)).entrySet()) { String tagName = itemEntry.getKey().replace(".json", ""); Datapack.Tag itemTag = itemEntry.getValue(); Key namespacedTag = Key.key(namespace, tagName); if (namespacedTag.equals(tagNamespace)) { return resolveTagItems(datapack, itemTag); } } } return null; } private static Optional findInJsonData(String file, Datapack datapack, Function> getFolder) { Key namespaceID = Key.key(file); for (var entry : datapack.namespacedData().entrySet()) { // Ensure the namespaces match String namespace = entry.getKey(); if (!namespaceID.namespace().equals(namespace)) { continue; } // get the folder var data = entry.getValue(); FileSystem folder = getFolder.apply(data); String targetFile = namespaceID.value(); while (targetFile.contains("/")) { String targetFolder = targetFile.substring(0, targetFile.indexOf('/')); targetFile = targetFile.substring(targetFile.indexOf('/') + 1); folder = folder.folder(targetFolder); } for (String fileName : folder.files()) { // ensure we are working with a json file if (!fileName.endsWith(".json")) { continue; } String fileId = fileName.substring(0, fileName.length() - 5); if (fileId.equals(targetFile)) { return Optional.of(folder.file(fileName)); } } } return Optional.empty(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/advancement/Advancement.java ================================================ package net.minestom.vanilla.datapack.advancement; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import net.minestom.vanilla.datapack.loot.function.Predicate; import net.minestom.vanilla.datapack.tags.ConditionsFor; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; public record Advancement(Display display, @Nullable String parent, @Nullable Criteria criteria, @Nullable List> requirements, Rewards rewards) { public record Display(Display.Icon icon, Component title, @Nullable String frame, String background, Component description, @Nullable Boolean showToast, @Nullable Boolean announceToChat, @Nullable Boolean hidden) { public record Icon(Key item, String nbt) { } } public record Criteria(Trigger trigger, TD conditions) { } @SuppressWarnings("unused") public interface Trigger { static Trigger from(String trigger) { return switch (trigger) { case "minecraft:allay_drop_item_on_block" -> ALLAY_DROP_ITEM_ON_BLOCK; case "minecraft:avoid_vibration" -> AVOID_VIBRATION; case "minecraft:bee_nest_destroyed" -> BEE_NEST_DESTROYED; case "minecraft:bred_animals" -> BRED_ANIMALS; case "minecraft:brewed_potion" -> BREWED_POTION; case "minecraft:changed_dimension" -> CHANGED_DIMENSION; case "minecraft:channeled_lightning" -> CHANNELED_LIGHTNING; case "minecraft:construct_beacon" -> CONSTRUCT_BEACON; case "minecraft:consume_item" -> CONSUME_ITEM; case "minecraft:cured_zombie_villager" -> CURED_ZOMBIE_VILLAGER; case "minecraft:effects_changed" -> EFFECTS_CHANGED; case "minecraft:enchanted_item" -> ENCHANTED_ITEM; case "minecraft:enter_block" -> ENTER_BLOCK; case "minecraft:entity_hurt_player" -> ENTITY_HURT_PLAYER; case "minecraft:entity_killed_player" -> ENTITY_KILLED_PLAYER; case "minecraft:filled_bucket" -> FILLED_BUCKET; case "minecraft:fishing_rod_hooked" -> FISHING_ROD_HOOKED; case "minecraft:hero_of_the_village" -> HERO_OF_THE_VILLAGE; case "minecraft:impossible" -> IMPOSSIBLE; case "minecraft:inventory_changed" -> INVENTORY_CHANGED; case "minecraft:item_durability_changed" -> ITEM_DURABILITY_CHANGED; case "minecraft:levitation" -> LEVITATION; case "minecraft:location" -> LOCATION; case "minecraft:nether_travel" -> NETHER_TRAVEL; case "minecraft:placed_block" -> PLACED_BLOCK; case "minecraft:player_hurt_entity" -> PLAYER_HURT_ENTITY; case "minecraft:player_killed_entity" -> PLAYER_KILLED_ENTITY; case "minecraft:recipe_unlocked" -> RECIPE_UNLOCKED; case "minecraft:shot_crossbow" -> SHOT_CROSSBOW; case "minecraft:slept_in_bed" -> SLEPT_IN_BED; case "minecraft:summoned_entity" -> SUMMONED_ENTITY; case "minecraft:tame_animal" -> TAME_ANIMAL; case "minecraft:tick" -> TICK; case "minecraft:used_ender_eye" -> USED_ENDER_EYE; case "minecraft:used_totem" -> USED_TOTEM; case "minecraft:villager_trade" -> VILLAGER_TRADE; default -> throw new IllegalArgumentException("Unknown trigger: " + trigger); }; } String trigger(); // allay_drop_item_on_block Trigger ALLAY_DROP_ITEM_ON_BLOCK = () -> "minecraft:allay_drop_item_on_block"; // avoid_vibration Trigger AVOID_VIBRATION = () -> "minecraft:avoid_vibration"; // bee_nest_destroyed Trigger BEE_NEST_DESTROYED = () -> "minecraft:bee_nest_destroyed"; // bred_animals Trigger BRED_ANIMALS = () -> "minecraft:bred_animals"; // brewed_potion Trigger BREWED_POTION = () -> "minecraft:brewed_potion"; // changed_dimension Trigger CHANGED_DIMENSION = () -> "minecraft:changed_dimension"; // channeled_lightning Trigger CHANNELED_LIGHTNING = () -> "minecraft:channeled_lightning"; // construct_beacon Trigger CONSTRUCT_BEACON = () -> "minecraft:construct_beacon"; // consume_item Trigger CONSUME_ITEM = () -> "minecraft:consume_item"; // cured_zombie_villager Trigger CURED_ZOMBIE_VILLAGER = () -> "minecraft:cured_zombie_villager"; // effects_changed Trigger EFFECTS_CHANGED = () -> "minecraft:effects_changed"; // enchanted_item Trigger ENCHANTED_ITEM = () -> "minecraft:enchanted_item"; // enter_block Trigger ENTER_BLOCK = () -> "minecraft:enter_block"; // entity_hurt_player Trigger ENTITY_HURT_PLAYER = () -> "minecraft:entity_hurt_player"; // entity_killed_player Trigger ENTITY_KILLED_PLAYER = () -> "minecraft:entity_killed_player"; // fall_from_height Trigger FALL_FROM_HEIGHT = () -> "minecraft:fall_from_height"; // filled_bucket Trigger FILLED_BUCKET = () -> "minecraft:filled_bucket"; // fishing_rod_hooked Trigger FISHING_ROD_HOOKED = () -> "minecraft:fishing_rod_hooked"; // hero_of_the_village Trigger HERO_OF_THE_VILLAGE = () -> "minecraft:hero_of_the_village"; // impossible Trigger IMPOSSIBLE = () -> "minecraft:impossible"; // inventory_changed Trigger INVENTORY_CHANGED = () -> "minecraft:inventory_changed"; // item_durability_changed Trigger ITEM_DURABILITY_CHANGED = () -> "minecraft:item_durability_changed"; // item_used_on_block Trigger ITEM_USED_ON_BLOCK = () -> "minecraft:item_used_on_block"; // kill_mob_near_sculk_catalyst Trigger KILL_MOB_NEAR_SCULK_CATALYST = () -> "minecraft:kill_mob_near_sculk_catalyst"; // killed_by_crossbow Trigger KILLED_BY_CROSSBOW = () -> "minecraft:killed_by_crossbow"; // levitation Trigger LEVITATION = () -> "minecraft:levitation"; // lightning_strike Trigger LIGHTNING_STRIKE = () -> "minecraft:lightning_strike"; // location Trigger LOCATION = () -> "minecraft:location"; // nether_travel Trigger NETHER_TRAVEL = () -> "minecraft:nether_travel"; // placed_block Trigger PLACED_BLOCK = () -> "minecraft:placed_block"; // player_generates_container_loot Trigger PLAYER_GENERATES_CONTAINER_LOOT = () -> "minecraft:player_generates_container_loot"; // player_hurt_entity Trigger PLAYER_HURT_ENTITY = () -> "minecraft:player_hurt_entity"; // player_interacted_with_entity Trigger PLAYER_INTERACTED_WITH_ENTITY = () -> "minecraft:player_interacted_with_entity"; // player_killed_entity Trigger PLAYER_KILLED_ENTITY = () -> "minecraft:player_killed_entity"; // recipe_unlocked Trigger RECIPE_UNLOCKED = () -> "minecraft:recipe_unlocked"; // ride_entity_in_lava Trigger RIDE_ENTITY_IN_LAVA = () -> "minecraft:ride_entity_in_lava"; // shot_crossbow Trigger SHOT_CROSSBOW = () -> "minecraft:shot_crossbow"; // slept_in_bed Trigger SLEPT_IN_BED = () -> "minecraft:slept_in_bed"; // slide_down_block Trigger SLIDE_DOWN_BLOCK = () -> "minecraft:slide_down_block"; // started_riding Trigger STARTED_RIDING = () -> "minecraft:started_riding"; // summoned_entity Trigger SUMMONED_ENTITY = () -> "minecraft:summoned_entity"; // tame_animal Trigger TAME_ANIMAL = () -> "minecraft:tame_animal"; // target_hit Trigger TARGET_HIT = () -> "minecraft:target_hit"; // thrown_item_picked_up_by_entity Trigger THROWN_ITEM_PICKED_UP_BY_ENTITY = () -> "minecraft:thrown_item_picked_up_by_entity"; // thrown_item_picked_up_by_player Trigger THROWN_ITEM_PICKED_UP_BY_PLAYER = () -> "minecraft:thrown_item_picked_up_by_player"; // tick Trigger TICK = () -> "minecraft:tick"; // used_ender_eye Trigger USED_ENDER_EYE = () -> "minecraft:used_ender_eye"; // used_totem Trigger USED_TOTEM = () -> "minecraft:used_totem"; // using_item Trigger USING_ITEM = () -> "minecraft:using_item"; // villager_trade Trigger VILLAGER_TRADE = () -> "minecraft:villager_trade"; // voluntary_exile Trigger VOLUNTARY_EXILE = () -> "minecraft:voluntary_exile"; static Trigger fromJson(JsonReader reader) throws IOException { String trigger = reader.nextString(); return Advancement.Trigger.from(trigger); } } public sealed interface Conditions { // Triggers when an allay drops an item on a block. Available extra conditions: // // conditions: // location: The location at the center of the block the item was dropped on. // Tags common to all locations[ // // ] // // item: The item dropped on the block. // // All possible conditions for items[ // //] record AllayDropItemOnBlock(ConditionsFor.Location location, ConditionsFor.Item item) implements Conditions { } // Triggers when a vibration event is ignored because the source player is crouching. No extra conditions. record AvoidVibration() implements Conditions { } // Triggers when the player breaks a bee nest or beehive. Available extra conditions: // // conditions: // block: Checks the block that was destroyed. Accepts block IDs. // item: The item used to break the block. // All possible conditions for items[ // // ] // // num_bees_inside: The number of bees that were inside the bee nest/beehive before it was broken. // // num_bees_inside: Another form for num_bees_inside. // max: The maximum value. // min: The minimum value. record BeeNestDestroyed(Block block, ConditionsFor.Item item, Count num_bees_inside) implements Conditions { public sealed interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // Triggers after the player breeds 2 animals. Available extra conditions: // // conditions: // child: Checks properties of the child that results from the breeding. // All possible conditions for entities[ // // ] // // child: Another format for "child". Specifies a list of predicates that must pass in order for the criterion to be granted. The origin of the predicate is the position of the player that would get the advancement. // // : A single predicate. // // parent: The parent. // // All possible conditions for entities[ // // ] // // parent: Another format for "parent". Specifies a list of predicates that must pass in order for the criterion to be granted. The origin of the predicate is the position of the player that would get the advancement. // // : A single predicate. // // partner: The partner (The entity the parent was bred with). // // All possible conditions for entities[ // // ] // // partner: Another format for "partner". Specifies a list of predicates that must pass in order for the criterion to be granted. The origin of the predicate is the position of the player that would get the advancement. // // : A single predicate. record BredAnimals(JsonUtils.ObjectOrList child, JsonUtils.ObjectOrList parent, JsonUtils.ObjectOrList partner) implements Conditions { } // minecraft:brewed_potion // //Triggers after the player takes any item out of a brewing stand. Available extra conditions: // // conditions: // potion: A brewed potion ID. record BrewedPotion(Key potion) implements Conditions { } // Triggers after the player travels between two dimensions. Available extra conditions: // // conditions: // from: The dimension the entity traveled from. This tag is a resource location for a dimension (only these in vanilla; more can be added with data packs). // to: The dimension the entity traveled to. Same accepted values as above. record ChangedDimension(Key from, Key to) implements Conditions { } // Triggers after the player successfully uses the Channeling enchantment on an entity or a lightning rod. Available extra conditions: // // conditions: // victims: The victims hit by the lightning summoned by the Channeling enchantment. All entities in this list must be hit. // : A victim. // All possible conditions for entities[ // // ] // //: Another format for the victim. Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the victim hit by the lighting, with the origin being the position of the player that would get the advancement. // // : A single predicate. record ChanneledLightning(List> victims) implements Conditions { } // Triggers after the player changes the structure of a beacon. (When the beacon updates itself). Available extra conditions: // // conditions: // level: The level of the updated beacon structure. // level: Another format. // max: The maximum value. // min: The minimum value. record ConstructBeacon(Count level) implements Conditions { public sealed interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // Triggers when the player consumes an item. Available extra conditions: // // conditions: // item: The item that was consumed. // All possible conditions for items[ // //] record ConsumeItem(ConditionsFor.Item item) implements Conditions { } // Triggers when the player cures a zombie villager. Available extra conditions: // // conditions: // villager: The villager that is the result of the conversion. The 'type' tag is redundant since it will always be "villager". // All possible conditions for entities[ // // ] // // villager: Another format for "villager". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the villager, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // zombie: The zombie villager right before the conversion is complete (not when it is initiated). The 'type' tag is redundant since it will always be "zombie_villager". // // All possible conditions for entities[ // // ] // // zombie: Another format for "zombie". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the zombie villager, with the origin being the position of the player that would get the advancement. // // : A single predicate. record CuredZombieVillager(JsonUtils.ObjectOrList villager, JsonUtils.ObjectOrList zombie) implements Conditions { } // Triggers after the player gets a status effect applied or taken from them. Available extra conditions: // // conditions: // effects: A list of active status effects the player currently has. // : The key name is a status effect name. // ambient: Whether the effect is from a beacon. // amplifier: The effect amplifier. // amplifier: Another format. // max: The maximum value. // min: The minimum value. // duration: The effect duration in ticks. // duration: Another format. // max: The maximum value. // min: The minimum value. // visible: Whether the effect has visible particles. // source: The entity that was the source of the status effect. When there is no entity or when the effect was self-applied or removed, the test passes only if the source is not specified. // All possible conditions for entities[ // // ] // // source: Another format for "source". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the source, with the origin being the position of the player that would get the advancement. // // : A single predicate. record EffectsChanged(Map effects, JsonUtils.ObjectOrList source) implements Conditions { public record Effect(boolean ambient, Count amplifier, Count duration, boolean visible) { public interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } } // minecraft:enchanted_item // //Triggers after the player enchants an item through an enchanting table (does not get triggered through an anvil, or through commands). Available extra conditions: // // conditions: // item: The item after it has been enchanted. // All possible conditions for items[ // // ] // // levels: The levels spent by the player on the enchantment. // levels: Another format. // // max: The maximum value. // min: The minimum value. record EnchantedItem(ConditionsFor.Item item, Count levels) implements Conditions { public interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // minecraft:enter_block // //Every tick, triggers once for each block the player's hitbox is inside (up to 12 blocks, the maximum number of blocks the player can stand in). Available extra conditions: // // conditions: // block: The block that the player is standing in. Accepts block IDs. // state: A map of block property names to values. Errors if the block doesn't have these properties. // key: Block property key and value pair. // key: Another format. // max: A maximum value. // min: A minimum value. record EnterBlock(Block block, Map state) implements Conditions { public interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // minecraft:entity_hurt_player // //Triggers after a player gets hurt, even without a source entity. Available extra conditions: // // conditions: // damage: Checks the damage done to the player. // Damage tags[ // //] record EntityHurtPlayer(ConditionsFor.Damage damage) implements Conditions { } // minecraft:entity_killed_player // //Triggers after a living entity kills a player. Available extra conditions: // // conditions: // entity: Checks the entity that was the source of the damage that killed the player (for example: The skeleton that shot the arrow). // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity that kills the player, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // killing_blow: Checks the type of damage that killed the player. // // Tags common to all damage types[ // //] record EntityKilledPlayer(JsonUtils.ObjectOrList entity, ConditionsFor.DamageTypes killing_blow) implements Conditions { } // minecraft:fall_from_height // //Triggers when a player lands after falling. Available extra conditions: // // conditions: // start_position: A location predicate for the last position before the falling started. // Tags common to all locations[ // // ] // // distance: The distance between the start position and the player's position. // // Distance predicate tags[ // //] record FallFromHeight(ConditionsFor.Location start_position, ConditionsFor.Distance distance) implements Conditions { } // minecraft:filled_bucket // //Triggers after the player fills a bucket. Available extra conditions: // // conditions: // item: The item resulting from filling the bucket. // All possible conditions for items[ // //] record FilledBucket(ConditionsFor.Item item) implements Conditions { } // minecraft:fishing_rod_hooked // //Triggers after the player successfully catches an item with a fishing rod or pulls an entity with a fishing rod. Available extra conditions: // // conditions: // entity: The entity that was pulled, or the fishing bobber if no entity is pulled. // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity pulled or the bobber, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // item: The item that was caught. // // All possible conditions for items[ // // ] // // rod: The fishing rod used. // // All possible conditions for items[ // //] record FishingRodHooked(JsonUtils.ObjectOrList entity, ConditionsFor.Item item, ConditionsFor.Item rod) implements Conditions { } // minecraft:hero_of_the_village // //Triggers when a raid ends in victory and the player has attacked at least one raider from that raid. No extra conditions. record HeroOfTheVillage() implements Conditions { } // minecraft:impossible // //Never triggers. No available conditions. record Impossible() implements Conditions { } // minecraft:inventory_changed // //Triggers after any changes happen to the player's inventory. Available extra conditions: // // conditions: // items: A list of items in the player's inventory. All items in the list must be in the player's inventory, but not all items in the player's inventory have to be in this list. // : An item stack. // All possible conditions for items[ // // ] // // slots: // // empty: The amount of slots empty in the inventory. // empty: Another format. // max: The maximum value. // min: The minimum value. // full: The amount of slots completely filled (stacksize) in the inventory. // full: Another format. // max: The maximum value. // min: The minimum value. // occupied: The amount of slots occupied in the inventory. // occupied: Another format. // max: The maximum value. // min: The minimum value. record InventoryChanged(List items, Slots slots) implements Conditions { public record Slots(Count empty, Count full, Count occupied) { interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } } // minecraft:item_durability_changed // //Triggers after any item in the inventory has been damaged in any form. Available extra conditions: // // conditions: // delta: The change in durability (negative numbers are used to indicate a decrease in durability). // delta: Another format. // max: The maximum value. // min: The minimum value. // durability: The remaining durability of the item. // durability: Another format. // max: The maximum value. // min: The minimum value. // item: The item before it was damaged, allows you to check the durability before the item was damaged. // All possible conditions for items[ // //] record ItemDurabilityChanged(Count delta, Count durability, ConditionsFor.Item item) implements Conditions { public record Count(Count.Value value, Count.Range range) { public record Value(int value) { } public record Range(int min, int max) { } } } // minecraft:item_used_on_block // //Triggers when the player uses their hand or an item on a block. Available extra conditions: // // conditions: // location: The location at the center of the block the item was used on. // Tags common to all locations[ // // ] // // item: The item used on the block. // // All possible conditions for items[ // //] record ItemUsedOnBlock(ConditionsFor.Location location, ConditionsFor.Item item) implements Conditions { } // minecraft:kill_mob_near_sculk_catalyst // //Triggers after a player is the source of a mob or player being killed within the range of a sculk catalyst. Available extra conditions: // // conditions: // entity: The entity that was killed. // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the mob, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // killing_blow: The type of damage that killed an entity. // // Tags common to all damage types[ // //] record KillMobNearSculkCatalyst(JsonUtils.ObjectOrList entity, ConditionsFor.Damage killing_blow) implements Conditions { } // minecraft:killed_by_crossbow // //Triggers after the player kills a mob or player using a crossbow in ranged combat. Available extra conditions: // // conditions: // unique_entity_types: The exact count of types of entities killed. // unique_entity_types: Another format. The acceptable range of count of types of entities killed. // max: The maximum value. // min: The minimum value. // victims: A list of victims. All of the entries must be matched, and one killed entity may match only one entry. // : A killed entities. // All possible conditions for entities[ // // ] // //: Another format for the victim. Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the victim, with the origin being the position of the player that would get the advancement. // // : A single predicate. record KilledByCrossbow(Count unique_entity_types, List> victims) implements Conditions { public interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // minecraft:levitation // //Triggers when the player has the levitation status effect. Available extra conditions: // // conditions: // distance: The distance between the position where the player started levitating and the player's current position. // Distance predicate tags[ // // ] // // duration: The duration of the levitation in ticks. // duration: Another format. // // max: The maximum value. // min: The minimum value. record Levitation(ConditionsFor.Distance distance, Count duration) implements Conditions { public interface Count { record Value(int value) implements Count { } record Range(int min, int max) implements Count { } } } // minecraft:lightning_strike // //Triggers when a lightning bolt disappears from the world, only for players within a 256 block radius of the lightning bolt. Available extra conditions: // // conditions: // lightning: The lightning bolt that disappeared. // All possible conditions for entities[ // // ] // // lightning: Another format for "lightning". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the lightning, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // bystander: An entity not hurt by the lightning strike but in a certain area around it. // // All possible conditions for entities[ // // ] // // bystander: Another format for "bystander". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the bystander, with the origin being the position of the player that would get the advancement. // // : A single predicate. record LightningStrike(JsonUtils.ObjectOrList lightning, JsonUtils.ObjectOrList bystander) implements Conditions { } // minecraft:location // //Triggers every 20 ticks (1 second). No extra conditions. record Location() implements Conditions { } // minecraft:nether_travel // //Triggers when the player travels to the Nether and then returns to the Overworld. Available extra conditions: // // conditions: // start_position: A location predicate for the last position before the player teleported to the Nether. // Tags common to all locations[ // // ] // // distance: The distance between the position where the player teleported to the Nether and the player's position when they returned. // // Distance predicate tags[ // //] record NetherTravel(ConditionsFor.Location start_position, ConditionsFor.Distance distance) implements Conditions { } // minecraft:placed_block // //Triggers when the player places a block. Available extra conditions: // // conditions: // block: The block that was placed. Accepts block IDs. // item: The item that was used to place the block before the item was consumed. // All possible conditions for items[ // // ] // // location: The location of the block that was placed. // // Tags common to all locations[ // // ] // // state: A map of block property names to values. Errors if the block doesn't have these properties. // // key: Block property key and value pair. // key: Another format. // max: A maximum value. // min: A minimum value. record PlacedBlock(Block block, ConditionsFor.Item item, ConditionsFor.Location location, Map state) implements Conditions { public interface Property { record Value(String value) implements Property { } record Range(String min, String max) implements Property { } } } // minecraft:player_generates_container_loot // //Triggers when the player generates the contents of a container with a loot table set. Available extra conditions: // // conditions: // loot_table*: The resource location of the generated loot table. record PlayerGeneratesContainerLoot(Key loot_table) implements Conditions { } // minecraft:player_hurt_entity // //Triggers after the player hurts a mob or player. Available extra conditions: // // conditions: // damage: The damage that was dealt. // Damage tags[ // // ] // // entity: The entity that was damaged. // // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record PlayerHurtEntity(ConditionsFor.Damage damage, JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:player_interacted_with_entity // //Triggers when the player interacts with an entity. Available extra conditions: // // conditions: // item: The item which was in the player's hand during interaction. // All possible conditions for items[ // // ] // // entity: The entity which was interacted with. // // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record PlayerInteractedWithEntity(ConditionsFor.Item item, JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:player_killed_entity // //Triggers after a player is the source of a mob or player being killed. Available extra conditions: // // conditions: // entity: The entity that was killed. // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. // // killing_blow: The type of damage that killed an entity. // // Tags common to all damage types[ // //] record PlayerKilledEntity(JsonUtils.ObjectOrList entity, ConditionsFor.Damage killing_blow) implements Conditions { } // minecraft:recipe_unlocked // //Triggers after the player unlocks a recipe (using a knowledge book for example). Available extra conditions: // // conditions: // recipe*: The recipe that was unlocked. record RecipeUnlocked(Key recipe) implements Conditions { } // minecraft:ride_entity_in_lava // //Triggers when a player mounts an entity walking on lava and while the entity moves with them. Available extra conditions: // // conditions: // start_position: A location predicate for the last position before the player mounted the entity. // Tags common to all locations[ // // ] // // distance: The distance between the start position and the player's position. // // Distance predicate tags[ // //] record RideEntityInLava(ConditionsFor.Location start_position, ConditionsFor.Distance distance) implements Conditions { } // minecraft:shot_crossbow // //Triggers when the player shoots a crossbow. Available extra conditions: // // conditions: // item: The crossbow that is used. // All possible conditions for items[ // //] record ShotCrossbow(ConditionsFor.Item item) implements Conditions { } // minecraft:slept_in_bed // //Triggers when the player enters a bed. No extra conditions. record SleptInBed() implements Conditions { } // minecraft:slide_down_block // //Triggers when the player slides down a block. Available extra conditions: // // conditions: // block: The block that the player slid on. // state: A map of block property names to values. Errors if the block doesn't have these properties. // key: Block property key and value pair. // key: Another format. // max: A maximum value. // min: A minimum value. record SlideDownBlock(Block block, Map state) implements Conditions { public interface Property { record Value(String value) implements Property { } record Range(String min, String max) implements Property { } } } // minecraft:started_riding // //Triggers when the player starts riding a vehicle or an entity starts riding a vehicle currently ridden by the player. No extra conditions. record StartedRiding() implements Conditions { } // minecraft:summoned_entity // //Triggers after an entity has been summoned. Works with iron golems (pumpkin and iron blocks), snow golems (pumpkin and snow blocks), the ender dragon (end crystals) and the wither (wither skulls and soul sand/soul soil). Using dispensers, commands, or pistons to place the wither skulls or pumpkins will still activate this trigger. Available extra conditions: // // conditions: // entity: The summoned entity. // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record SummonedEntity(JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:tame_animal // //Triggers after the player tames an animal. Available extra conditions: // // conditions: // entity: Checks the entity that was tamed. // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record TameAnimal(JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:target_hit // //Triggers when the player shoots a target block. Available extra conditions: // // conditions: // signal_strength: The redstone signal that will come out of the target block. // signal_strength: Another format. // max: The maximum value. // min: The minimum value. // projectile: The projectile hit the target block. // All possible conditions for entities[ // // ] // // projectile: Another format for "projectile". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the projectile, with the origin being the position of the player that would get the advancement. // // : A single predicate. record TargetHit(int signal_strength, JsonUtils.ObjectOrList projectile) implements Conditions { } // minecraft:thrown_item_picked_up_by_entity // //Triggers after the player throws an item and another entity picks it up. Available extra conditions: // // conditions: // item: The thrown item which was picked up. // All possible conditions for items[ // // ] // // entity: The entity which picked up the item. // // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record ThrownItemPickedUpByEntity(ConditionsFor.Item item, JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:thrown_item_picked_up_by_player // //Triggers when a player picks up an item thrown by another entity. Available extra conditions: // // conditions: // item: The item thrown. // All possible conditions for items[ // // ] // // entity: The entity that threw the item. // // All possible conditions for entities[ // // ] // // entity: Another format for "entity". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the entity, with the origin being the position of the player that would get the advancement. // // : A single predicate. record ThrownItemPickedUpByPlayer(ConditionsFor.Item item, JsonUtils.ObjectOrList entity) implements Conditions { } // minecraft:tick // //Triggers every tick (20 times a second). No extra conditions. record Tick() implements Conditions { } // minecraft:used_ender_eye // //Triggers when the player uses an eye of ender (in a world where strongholds generate). Available extra conditions: // // conditions: // distance: The horizontal distance between the player and the stronghold. // distance: Another format. // max: A maximum value. // min: A minimum value. record UsedEnderEye(Count distance) implements Conditions { public interface Count { record Value(double value) implements Count { } record Range(double min, double max) implements Count { } } } // minecraft:used_totem // //Triggers when the player uses a totem. Available extra conditions: // // conditions: // item: The item, only works with totem items. // All possible conditions for items[ // //] record UsedTotem(ConditionsFor.Item item) implements Conditions { } // minecraft:using_item // //Triggers for every tick that the player uses an item that is used continuously. It is known to trigger for bows, crossbows, honey bottles, milk buckets, potions, shields, spyglasses, tridents, food items, eyes of ender, etc. Most items that activate from a single click, such as fishing rods, do not affect this trigger. Available extra conditions: // // conditions: // item: The item that is used. // All possible conditions for items[ // //] record UsingItem(ConditionsFor.Item item) implements Conditions { } // minecraft:villager_trade // //Triggers after the player trades with a villager or a wandering trader. Available extra conditions: // // conditions: // item: The item that was purchased. The "count" tag checks the count from one trade, not multiple. // All possible conditions for items[ // // ] // // villager: The villager the item was purchased from. // // All possible conditions for entities[ // // ] // // villager: Another format for "villager". Specifies a list of predicates that must pass in order for the criterion to be granted. The checks are applied to the villager, with the origin being the position of the player that would get the advancement. // // : A single predicate. record VillagerTrade(ConditionsFor.Item item, JsonUtils.ObjectOrList villager) implements Conditions { } // minecraft:voluntary_exile // //Triggers when the player causes a raid. No extra conditions. record VoluntaryExile() implements Conditions { } } public record Rewards(List recipes, List loot, @Optional Integer experience, String function) { } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/dimension/DimensionType.java ================================================ package net.minestom.vanilla.datapack.dimension; import net.minestom.vanilla.datapack.number.NumberProvider; // { // "ambient_light": 0.0, // "bed_works": true, // "coordinate_scale": 1.0, // "effects": "minecraft:overworld", // "has_ceiling": false, // "has_raids": true, // "has_skylight": true, // "height": 384, // "infiniburn": "#minecraft:infiniburn_overworld", // "logical_height": 384, // "min_y": -64, // "monster_spawn_block_light_limit": 0, // "monster_spawn_light_level": { // "type": "minecraft:uniform", // "value": { // "max_inclusive": 7, // "min_inclusive": 0 // } // }, // "natural": true, // "piglin_safe": false, // "respawn_anchor_works": false, // "ultrawarm": false //} public record DimensionType( double ambient_light, boolean bed_works, double coordinate_scale, String effects, boolean has_ceiling, boolean has_raids, boolean has_skylight, int height, String infiniburn, int logical_height, int min_y, int monster_spawn_block_light_limit, NumberProvider.Int monster_spawn_light_level, boolean natural, boolean piglin_safe, boolean respawn_anchor_works, boolean ultrawarm ) { } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/json/JsonUtils.java ================================================ package net.minestom.vanilla.datapack.json; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.DatapackLoader; import okio.Buffer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; public class JsonUtils { public static JsonReader jsonReader(String source) { try (Buffer buffer = new Buffer().writeUtf8(source)) { return JsonReader.of(buffer); } } public interface ObjectOrList { /** Throws an exception if this is not an object */ boolean isObject(); O asObject(); boolean isList(); /** Throws an exception if this is not a list */ List asList(); } public interface SingleOrList extends ObjectOrList, ListLike { static SingleOrList fromJson(Type elementType, JsonReader reader) throws IOException { JsonReader.Token peek = reader.peek(); if (peek != JsonReader.Token.BEGIN_ARRAY) { return new Single<>(DatapackLoader.moshi(elementType).apply(reader)); } Stream.Builder builder = Stream.builder(); reader.beginArray(); while (reader.hasNext()) { builder.add(DatapackLoader.moshi(elementType).apply(reader)); } reader.endArray(); return new List<>(builder.build().toList()); } record Single(O object) implements SingleOrList { @Override public boolean isObject() { return true; } @Override public O asObject() { return object; } @Override public boolean isList() { return false; } @Override public java.util.List asList() { throw new IllegalStateException("Not a list"); } @Override public java.util.@NotNull List list() { return java.util.List.of(object); } } record List(java.util.List list) implements SingleOrList { @Override public boolean isObject() { return false; } @Override public L asObject() { throw new IllegalStateException("Not an object"); } @Override public boolean isList() { return true; } @Override public java.util.List asList() { return list; } @Override public java.util.@NotNull List list() { return list; } } } public interface IoFunction { R apply(T t) throws IOException; } public static T unionStringType(JsonReader reader, String key, Function> findReader) throws IOException { return unionMapType(reader, key, json -> { String value = json.nextString(); if (value == null) return null; return Key.key(value).toString(); }, findReader); } public static T unionStringTypeAdapted(JsonReader reader, String key, Function> findReader) throws IOException { return unionStringType(reader, key, str -> { Class clazz = findReader.apply(str); if (clazz == null) return null; return DatapackLoader.moshi(clazz); }); } public static T unionStringTypeMap(JsonReader reader, String key, Map> map) throws IOException { return unionStringType(reader, key, map::get); } public static T unionStringTypeMapAdapted(JsonReader reader, String key, Map> map) throws IOException { Map> adaptedMap = map.entrySet().stream() .map(entry -> { var entryKey = entry.getKey(); var value = entry.getValue(); return Map.entry(entryKey, DatapackLoader.moshi(value)); }) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); return unionStringTypeMap(reader, key, adaptedMap); } public static T unionMapType(JsonReader reader, String key, IoFunction read, Function> findReader) throws IOException { // Fetch the property V property; try (JsonReader peek = reader.peekJson()) { peek.beginObject(); property = JsonUtils.findProperty(peek, key, read); if (property == null) throw new IOException("Expected property '" + key + "'"); } // Find the correct handler, and call it IoFunction readFunction = findReader.apply(property); if (readFunction == null) throw new IOException("Unknown type: " + property); return readFunction.apply(reader); } public static T typeMapMapped(JsonReader reader, Map> type2readFunction) throws IOException { return typeMap(reader, type2readFunction::get); } public static T typeMap(JsonReader reader, IoFunction> type2readFunction) throws IOException { JsonReader.Token token = reader.peek(); IoFunction readFunction = type2readFunction.apply(token); if (readFunction == null) throw new IllegalStateException("Unknown token type: " + token); return readFunction.apply(reader); } /** * Note that this method MUTATES the reader. * In order to safely use this method, you should call {@link JsonReader#peekJson()} and use that instead. */ public static @Nullable T findProperty(JsonReader reader, String property, IoFunction read) throws IOException { while (reader.hasNext() && reader.peek() != JsonReader.Token.END_OBJECT) { String name = reader.nextName(); if (name.equals(property)) { return read.apply(reader); } else { reader.skipValue(); } } return null; } public static boolean hasProperty(JsonReader reader, String property) throws IOException { JsonReader peek = reader.peekJson(); while (peek.hasNext() && peek.peek() != JsonReader.Token.END_OBJECT) { String name = peek.nextName(); if (name.equals(property)) { return true; } else { peek.skipValue(); } } return false; } public static Map readObjectToMap(JsonReader reader, IoFunction readValue) throws IOException { Map map = new HashMap<>(); reader.beginObject(); while (reader.hasNext() && reader.peek() != JsonReader.Token.END_OBJECT) { String key = reader.nextName(); T value = readValue.apply(reader); map.put(key, value); } reader.endObject(); return Collections.unmodifiableMap(map); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/json/ListLike.java ================================================ package net.minestom.vanilla.datapack.json; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; public interface ListLike extends List { /** * Returns a list containing all of the elements in this list, in proper sequence. */ @NotNull List list(); @Override default int size() { return list().size(); } @Override default boolean isEmpty() { return list().isEmpty(); } @Override default boolean contains(Object o) { return list().contains(o); } @NotNull @Override default Iterator iterator() { return list().iterator(); } @NotNull @Override default Object @NotNull [] toArray() { return list().toArray(); } @NotNull @Override default T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { return list().toArray(a); } @Override default boolean add(T t) { return list().add(t); } @Override default boolean remove(Object o) { return list().remove(o); } @Override default boolean containsAll(@NotNull Collection c) { //noinspection SlowListContainsAll return list().containsAll(c); } @Override default boolean addAll(@NotNull Collection c) { return list().addAll(c); } @Override default boolean addAll(int index, @NotNull Collection c) { return list().addAll(index, c); } @Override default boolean removeAll(@NotNull Collection c) { return list().removeAll(c); } @Override default boolean retainAll(@NotNull Collection c) { return list().retainAll(c); } @Override default void clear() { list().clear(); } @Override default T get(int index) { return list().get(index); } @Override default T set(int index, T element) { return list().set(index, element); } @Override default void add(int index, T element) { list().add(index, element); } @Override default T remove(int index) { return list().remove(index); } @Override default int indexOf(Object o) { return list().indexOf(o); } @Override default int lastIndexOf(Object o) { return list().lastIndexOf(o); } @NotNull @Override default ListIterator listIterator() { return list().listIterator(); } @NotNull @Override default ListIterator listIterator(int index) { return list().listIterator(index); } @NotNull @Override default List subList(int fromIndex, int toIndex) { return list().subList(fromIndex, toIndex); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/json/Optional.java ================================================ package net.minestom.vanilla.datapack.json; import com.squareup.moshi.JsonQualifier; @JsonQualifier public @interface Optional { } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/LootTable.java ================================================ package net.minestom.vanilla.datapack.loot; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.DatapackUtils; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.datapack.loot.function.LootFunction; import net.minestom.vanilla.datapack.loot.function.Predicate; import net.minestom.vanilla.datapack.number.NumberProvider; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Represents a loot table. * @param type Specifies the loot context in which the loot table should be invoked. All item modifiers, predicates and number providers are then validated to ensure the parameters of the context type specified here cover all requirements, and prints a warning message in the output log if any modifier or predicate requires a context parameter that is not covered. * @param functions Applies item modifiers in order, onto all item stacks dropped by this table. * @param pools A list of all pools for this loot table. Pools are applied in order. * @param random_sequence A resource location specifying the name of the random sequence that is used to generate loot from this loot table. If only one loot table uses a specific random sequence, the order of the randomized sets of items generated is the same for every world using the same world seed. If multiple loot tables use the same random sequence, the loot generated from any one of them changes depending on how many times and in what order any of the other loot tables were invoked. */ public record LootTable(@Nullable String type, @Nullable List functions, @Nullable List pools, @Nullable Key random_sequence) { public record Pool(@Nullable List conditions, @Nullable List functions, NumberProvider.Int rolls, NumberProvider.Double bonus_rolls, List entries) { public sealed interface Entry { @Nullable List conditions(); Key type(); static Pool.Entry fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch(type) { case "minecraft:item" -> Pool.Entry.Item.class; case "minecraft:tag" -> Pool.Entry.Tag.class; case "minecraft:loot_table" -> Pool.Entry.LootTableNested.class; case "minecraft:dynamic" -> Pool.Entry.Dynamic.class; case "minecraft:empty" -> Pool.Entry.Empty.class; case "minecraft:group" -> Pool.Entry.Group.class; case "minecraft:alternatives" -> Pool.Entry.Alternatives.class; case "minecraft:sequence" -> Pool.Entry.Sequence.class; default -> null; }); } sealed interface ItemGenerator extends Entry { @Nullable List functions(); @Nullable NumberProvider weight(); NumberProvider quality(); /** * Each element in this list is a singular loot entry. */ List> apply(Datapack datapack, LootContext context); } /** * item -> Provides a loot entry that drops a single item stack. * • functions: Invokes item functions to the item stack(s). * - An item function. The JSON structure of this object is described on the Item modifiers page. * • weight: Determines how often the loot entry is chosen out of all the entries in the pool. Entries with higher weights are used more often. The chance of an entry being chosen is [this entry's weight ÷ total of all considered entries' weights]. * • quality: Modifies the loot entry's weight based on the luck attribute of the killer_entity for loot context type entity or the this entity for all other loot table types. Formula is floor(weight + (quality × generic.luck)). * • name: The resource location of the item to be generated, e.g. minecraft:diamond. The default, if not changed by item functions, is a stack of 1 of the default instance of the item. */ record Item(List conditions, List functions, NumberProvider weight, NumberProvider quality, Key name, @Nullable Integer count) implements ItemGenerator { @Override public Key type() { return Key.key("minecraft:item"); } @Override public List> apply(Datapack datapack, LootContext context) { return List.of(List.of( ItemStack.of(Objects.requireNonNull(Material.fromKey(name)), count == null ? 1 : count) )); } } /** * tag -> Provides a loot entry that generates all item in an item tag, or multiple loot entries (each entry generates a single item in the item tag). * • functions: Invokes item functions to the item stack(s). * - An item function. The JSON structure of this object is described on the Item modifiers page. * • weight: Determines how often a loot entry is chosen out of all the entries in the pool. Entries with higher weights are used more often. The chance of an entry being chosen is [this entry's weight ÷ total of all considered entries' weights]. * • quality: Modifies the loot entry's weight based on the luck attribute of the killer_entity for loot context type entity or the this entity for all other loot table types. Formula is floor(weight + (quality × generic.luck)). * • name: The resource location of the item tag to query, e.g. minecraft:arrows. * • expand: If set to true, provides one loot entry per item in the tag with the same weight and quality, and each entry generates one item. If false, provides a single loot entry that generates all items (each with count of 1) in the tag. */ record Tag(List conditions, List functions, NumberProvider weight, NumberProvider quality, Key name, boolean expand) implements ItemGenerator { @Override public Key type() { return Key.key("minecraft:tag"); } @Override public List> apply(Datapack datapack, LootContext context) { List> result = new ArrayList<>(); var itemTags = DatapackUtils.findTags(datapack, "item", name); var items = itemTags.stream() .map(Material::fromKey) .filter(Objects::nonNull) .map(material -> ItemStack.of(material, 1)) .toList(); if (expand) { for (var item : items) { result.add(List.of(item)); } } else { result.add(items); } return List.copyOf(result); } } /** * loot_table -> Provides another loot table as a loot entry. * • functions: Invokes item functions to the item stack(s). * - An item function. The JSON structure of this object is described on the Item modifiers page. * • weight: Determines how often the loot entry is chosen out of all the entries in the pool. Entries with higher weights are used more often. The chance of an entry being chosen is [this entry's weight ÷ total of all considered entries' weights]. * • quality: Modifies the loot entry's weight based on the luck attribute of the killer_entity for loot context type entity or the this entity for all other loot table types. Formula is floor(weight + (quality × generic.luck)). * • name: The resource location of the loot table to be used, e.g. minecraft:gameplay/fishing/junk. */ record LootTableNested(List conditions, List functions, NumberProvider weight, NumberProvider quality, Key name) implements Pool.Entry { @Override public Key type() { return Key.key("minecraft:loot_table"); } } /** * dynamic -> Provides a loot entry that generates block-specific drops. * • functions: Invokes item functions to the item stack(s). * - An item function. The JSON structure of this object is described on the Item modifiers page. * • weight: Determines how often the loot entry is chosen out of all the entries in the pool. Entries with higher weights are used more often. The chance of an entry being chosen is [this entry's weight ÷ total of all considered entries' weights]. * • quality: Modifies the loot entry's weight based on the luck attribute of the killer_entity for loot context type entity or the this entity for all other loot table types. Formula is floor(weight + (quality × generic.luck)). * • name: Can be contents to drop block entity contents. */ record Dynamic(List conditions, List functions, NumberProvider weight, NumberProvider quality, String name) implements ItemGenerator { @Override public Key type() { return Key.key("minecraft:dynamic"); } @Override public List> apply(Datapack datapack, LootContext context) { Block blockEntity = context.get(LootContext.BLOCK_ENTITY); // TODO: Drop chest contents return List.of(List.of()); } } /** * empty -> Provides a loot entry that generates nothing into the loot pool. * • functions: Invokes item functions to the item stack(s). * - An item function. The JSON structure of this object is described on the Item modifiers page. * • weight: Determines how often the loot entry is chosen out of all the entries in the pool. Entries with higher weights are used more often. The chance of an entry being chosen is [this entry's weight ÷ total of all considered entries' weights]. * • quality: Modifies the loot entry's weight based on the luck attribute of the killer_entity for loot context type entity or the this entity for all other loot table types. Formula is floor(weight + (quality × generic.luck)). */ record Empty(List conditions, List functions, NumberProvider weight, NumberProvider quality) implements ItemGenerator { @Override public Key type() { return Key.key("minecraft:empty"); } @Override public List> apply(Datapack datapack, LootContext context) { return List.of(List.of()); } } /** * group -> All entry providers in the children list is applied into the loot pool. Can be used for convenience, e.g. if one condition applies for multiple entries. * • children: The list of entry providers. * - An entry provider. */ record Group(List conditions, List children) implements Pool.Entry { @Override public Key type() { return Key.key("minecraft:group"); } } /** * alternatives -> Only the first successful (conditions are met) entry provider, in order, is applied to the loot pool. * • children: The list of entry providers. * - An entry provider. */ record Alternatives(List conditions, List children) implements Pool.Entry { @Override public Key type() { return Key.key("minecraft:alternatives"); } } /** * sequence -> The child entry providers are applied to the loot pool in sequential order, continuing until an entry provider's conditions are not met, then applying no more entry providers from the children. * • children: The list of entry providers. * - An entry provider. */ record Sequence(List conditions, List children) implements Pool.Entry { @Override public Key type() { return Key.key("minecraft:sequence"); } } } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/NBTPath.java ================================================ package net.minestom.vanilla.datapack.loot; import com.squareup.moshi.JsonReader; import net.kyori.adventure.nbt.BinaryTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.StringReader; import java.util.Map; public interface NBTPath { static NBTPath fromJson(JsonReader reader) throws IOException { return NBTPathImpl.readPath(new StringReader(reader.nextString())); } /** * Indexes the given NBT using this path and returns the results. * * @param nbt the NBT to index * @return a map of the path to the resulting NBT */ @NotNull Map get(BinaryTag nbt); /** * A single path returns either a single NBT value or nothing. * This makes it possible to set a single value in a NBT structure as well. */ interface Single extends NBTPath { static Single fromJson(JsonReader reader) throws IOException { return NBTPathImpl.readSingle(new StringReader(reader.nextString())); } /** * Gets the single result of this path, or returns null if there is not exactly one result. * @param nbt the NBT to index * @return the single result, or null if there is not exactly one result */ @Nullable BinaryTag getSingle(BinaryTag nbt); @Override @Deprecated default @NotNull Map get(BinaryTag nbt) { BinaryTag single = getSingle(nbt); return single == null ? Map.of() : Map.of(this, single); } /** * Sets the value of this path in the given NBT to the given value. * @param nbt the NBT to set the value in * @param value the value to set * @return the new NBT, or null if the value could not be set i.e. the path did not exist */ @Nullable BinaryTag set(BinaryTag nbt, BinaryTag value); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/NBTPathImpl.java ================================================ package net.minestom.vanilla.datapack.loot; import it.unimi.dsi.fastutil.ints.IntSet; import net.kyori.adventure.nbt.*; import net.minestom.vanilla.datapack.nbt.NBTUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.StringReader; import java.util.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; interface NBTPathImpl extends NBTPath { static NBTPathImpl readPath(StringReader reader) throws IOException { return Reader.readPath(reader); } static Single readSingle(StringReader stringReader) throws IOException { if (!(Reader.readPath(stringReader) instanceof Single single)) { throw new IllegalArgumentException("Expected a single nbt path, got a multi nbt path"); } return single; } interface NbtPathCollector extends BiConsumer, BinaryTag> { } /** * Selects an arbitrary number of elements from a provided NBT element. */ interface Selector { /** * Provides each selected NBT element from {@code source} into {@code selectedElements}. * * @param source the reference that is the source NBT elemen t * @param selectedElements the consumer for selected NBT references */ void get(@NotNull T source, @NotNull NbtPathCollector selectedElements); /** * Used to determine if this selector can be used to select the provided {@code type}. * * @param type the type to check * @return true if this selector can be used to select the provided {@code type} */ boolean fitsGeneric(@NotNull BinaryTagType type); } /** * Selects a single element (or none) from a provided NBT element. * @param */ interface SingleSelector extends Selector { /** * Provides the selected NBT element from {@code source}. * * @param source the reference that is the source NBT element * @return the selected NBT element */ @Nullable BinaryTag get(@NotNull T source); @Override default void get(@NotNull T source, @NotNull NbtPathCollector selectedElements) { BinaryTag selected = get(source); if (selected != null) selectedElements.accept(this, selected); } } } record NBTPathMultiImpl(@NotNull List> selectors) implements NBTPathImpl { public @NotNull Map get(@NotNull BinaryTag source) { Map>, BinaryTag> references = Map.of(List.of(), source); for (var selector : selectors()) { Map>, BinaryTag> newReferences = new HashMap<>(); references.forEach((list, nbt) -> { if (!selector.fitsGeneric(nbt.type())) return; //noinspection unchecked ((NBTPathImpl.Selector) selector).get(nbt, (newSelector, newNbt) -> { List> newKey = new ArrayList<>(list); newKey.add(newSelector); newReferences.put(newKey, newNbt); }); }); if (newReferences.isEmpty()) return Map.of(); references = newReferences; } return references.entrySet().stream() .map(entry -> { NBTPath.Single path = new NBTPathSingleImpl(Collections.unmodifiableList(entry.getKey())); BinaryTag nbt = entry.getValue(); return Map.entry(path, nbt); }).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override public String toString() { return selectors().stream() .map(Selector::toString) .collect(Collectors.joining()); } } record NBTPathSingleImpl(List> selectors) implements NBTPathImpl, NBTPath.Single { @Override public @Nullable BinaryTag getSingle(BinaryTag nbt) { for (var selector : selectors()) { if (nbt == null || !selector.fitsGeneric(nbt.type())) { // path has failed return null; } //noinspection unchecked nbt = ((NBTPathImpl.SingleSelector) selector).get(nbt); } return nbt; } @Override public @Nullable BinaryTag set(BinaryTag nbt, BinaryTag value) { return retrieveModified(0, nbt, value); } /** * Retrieves the modified version of this nbt * @param i the index of the selector to use * @param container the container to modify * @param value the value to set * @return the modified version of the container */ private @Nullable BinaryTag retrieveModified(int i, BinaryTag container, BinaryTag value) { if (i == selectors.size()) return value; if (container == null) return null; SingleSelector selector = selectors.get(i); if (!selector.fitsGeneric(container.type())) return null; //noinspection unchecked BinaryTag nbt = ((SingleSelector) selector).get(container); if (nbt == null) return null; // handle the next id on a per-type basis if (nbt.type() == BinaryTagTypes.COMPOUND) { CompoundBinaryTag compound = (CompoundBinaryTag) nbt; String key; { // find the key if (selector instanceof RootKey root) key = root.key(); else if (selector instanceof CompoundKey compoundKey) key = compoundKey.key(); else if (selector instanceof CompoundFilter) key = null; else throw new IllegalStateException("Unknown selector type: " + selector.getClass()); } if (key == null) { // key is null, which mean that this is a filter and we can return the next call directly return retrieveModified(i + 1, compound, value); } // key is not null, which means that we need to set the value in the compound BinaryTag nextValue = retrieveModified(i + 1, compound.get(key), value); if (nextValue == null) return null; Map mutCompound = StreamSupport.stream(compound.spliterator(), false) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); mutCompound.put(key, nextValue); return CompoundBinaryTag.from(mutCompound); } if (nbt.type() == BinaryTagTypes.LIST) { ListBinaryTag list = (ListBinaryTag) nbt; int index; { // find the index if (selector instanceof ListIndex listIndex) index = listIndex.index(); else throw new IllegalStateException("Unknown selector type: " + selector.getClass()); } if (index < 0 || index >= list.size()) return null; BinaryTag nextValue = retrieveModified(i + 1, list.get(index), value); if (nextValue == null) return null; List javaList = new ArrayList<>(list.stream().toList()); javaList.set(index, nextValue); return ListBinaryTag.listBinaryTag(list.elementType(), javaList); } // the current nbt container is a value, which means we replace it directly with the value param return value; } } // selectors /** * Selects, if possible, the element under the {@link #key()} key of the provided source.
* * @param key the key to select */ record RootKey(@NotNull String key) implements NBTPathImpl.SingleSelector { @Override public @Nullable BinaryTag get(@NotNull CompoundBinaryTag source) { if (!source.keySet().contains(key)) return null; return source.get(key); } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return type == BinaryTagTypes.COMPOUND; } @Override public String toString() { return key; } } /** * Selects, if possible, the element under the {@link #key()} key of the provided source.
* * @param key the key to select */ record CompoundKey(@NotNull String key) implements NBTPathImpl.SingleSelector { @Override public @Nullable BinaryTag get(@NotNull CompoundBinaryTag source) { if (!source.keySet().contains(key)) return null; return source.get(key); } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return type == BinaryTagTypes.COMPOUND; } @Contract(pure = true) @Override public @NotNull String toString() { return "." + key; } } /** * Selects the provided source if it passes the {@link #filter()}.
* * @param filter the filter to use */ record CompoundFilter(@NotNull CompoundBinaryTag filter) implements NBTPathImpl.SingleSelector { @Override public @Nullable BinaryTag get(@NotNull BinaryTag source) { return NBTUtils.compareNBT(filter, source, false) ? source : null; } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return true; } @Override public @NotNull String toString() { try { return TagStringIO.get().asString(filter); } catch (IOException e) { throw new RuntimeException(e); } } } /** * Selects, if possible, the element of index {@link #index()} from the provided list, or, if the index is negative, * selects the nth element from the end of the list, where n is {@link #index()}.
* * @param index the index to select, or negative to select starting from the end */ record ListIndex(int index) implements NBTPathImpl.SingleSelector { @Override public @Nullable BinaryTag get(@NotNull ListBinaryTag source) { var newIndex = index >= 0 ? index : source.size() + index; if (newIndex < 0) return null; if (newIndex >= source.size()) return null; return source.get(newIndex); } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return type == BinaryTagTypes.LIST; } @Contract(pure = true) @Override public @NotNull String toString() { return "[" + index + "]"; } } /** * Selects, if possible, each element from the provided list that fits the {@link #filter()}.
* * @param filter the filter for each element in the list */ record ListFilter(@NotNull CompoundBinaryTag filter) implements NBTPathImpl.Selector { @Override public void get(@NotNull ListBinaryTag source, NBTPathImpl.@NotNull NbtPathCollector selectedElements) { IntStream.range(0, source.size()) .mapToObj(i -> Map.entry(i, source.get(i))) .filter(entry -> NBTUtils.compareNBT(filter, entry.getValue(), false)) .forEach(entry -> { int i = entry.getKey(); BinaryTag nbt = entry.getValue(); selectedElements.accept(new ListIndex(i), nbt); }); } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return type == BinaryTagTypes.LIST; } @Override public @NotNull String toString() { try { return "[" + TagStringIO.get().asString(filter) + "]"; } catch (IOException e) { throw new RuntimeException(e); } } } /** * Selects, if possible, every item from the provided list.
*/ record EntireList() implements NBTPathImpl.Selector { @Override public void get(@NotNull ListBinaryTag source, NBTPathImpl.@NotNull NbtPathCollector selectedElements) { IntStream.range(0, source.size()) .mapToObj(i -> Map.entry(i, source.get(i))) .forEach(entry -> { int i = entry.getKey(); BinaryTag nbt = entry.getValue(); selectedElements.accept(new ListIndex(i), nbt); }); } @Override public boolean fitsGeneric(@NotNull BinaryTagType type) { return type == BinaryTagTypes.LIST; } @Contract(pure = true) @Override public @NotNull String toString() { return "[]"; } } // Reading interface Reader { @NotNull IntSet VALID_SELECTOR_STARTERS = IntSet.of('.', '{', '['); @NotNull IntSet VALID_INTEGER_CHARACTERS = IntSet.of('-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); @NotNull IntSet INVALID_UNQUOTED_CHARACTERS = IntSet.of(-1, '.', '\'', '\"', '{', '}', '[', ']'); static @NotNull NBTPathImpl readPath(@NotNull StringReader reader) throws IOException { List> selectors = new ArrayList<>(); if (!VALID_SELECTOR_STARTERS.contains(peek(reader))) { var key = readString(reader); if (key != null) { selectors.add(new RootKey(key)); } } while (true) { reader.mark(0); if (!VALID_SELECTOR_STARTERS.contains(peek(reader))) { if (selectors.isEmpty()) { reader.reset(); String message = "NBT paths must contain at least one selector (reading from " + reader + ")"; throw new IllegalArgumentException(message); } // if all selectors are single, return a NBTPath.Single boolean allSingle = selectors.stream().allMatch(selector -> selector instanceof NBTPathImpl.Single); List> selectorsView = Collections.unmodifiableList(selectors); //noinspection unchecked return allSingle ? new NBTPathSingleImpl((List>) (List) selectorsView) : new NBTPathMultiImpl(selectorsView); } var selector = readPathSelector(reader); if (selector == null) { reader.reset(); String message = "Invalid NBT path selector (reading from " + reader + ")"; throw new IllegalArgumentException(message); } selectors.add(selector); } } // Returning null indicates a failure to read @SuppressWarnings("ResultOfMethodCallIgnored") static @Nullable NBTPathImpl.Selector readPathSelector(@NotNull StringReader reader) throws IOException { var firstChar = peek(reader); return switch (firstChar) { case '.' -> { reader.skip(1); // Skip period var string = readString(reader); yield string != null ? new CompoundKey(string) : null; } case '{' -> { var compound = NBTUtils.readCompoundSNBT(reader); yield compound != null ? new CompoundFilter(compound) : null; } case '[' -> { reader.skip(1); // Skip opening square brackets var secondChar = peek(reader); var selector = switch (secondChar) { case ']' -> new EntireList(); case '{' -> { var compound = NBTUtils.readCompoundSNBT(reader); yield compound != null ? new ListFilter(compound) : null; } default -> { if (VALID_INTEGER_CHARACTERS.contains(secondChar)) { var index = readInteger(reader); yield index != null ? new ListIndex(index) : null; } yield null; } }; reader.skip(1); // Skip closing square brackets yield selector; } default -> null; }; } @SuppressWarnings("ResultOfMethodCallIgnored") private static @Nullable Integer readInteger(@NotNull StringReader reader) throws IOException { StringBuilder builder = new StringBuilder(); int peek; while (VALID_INTEGER_CHARACTERS.contains(peek = reader.read())) { builder.appendCodePoint(peek); } // Unread the one extra character that was read; this does nothing if the entire string has been read reader.skip(-1); try { return Integer.parseInt(builder.toString()); } catch (NumberFormatException e) { return null; } } @SuppressWarnings("ResultOfMethodCallIgnored") private static @Nullable String readString(@NotNull StringReader reader) throws IOException { var peek = peek(reader); if (peek == -1) { return null; } StringBuilder builder = new StringBuilder(); if (peek == '"' || peek == '\'') { // Read quoted string reader.skip(1); // Skip the character we know already boolean escape = false; while (true) { var next = reader.read(); if (next == '\\') { // Read escape character escape = true; } else if (next == peek && !escape) { // Return if unescaped closing character return builder.toString(); } else { if (escape) { // If there was an unused escape, re-add it; there's only one character it's used for builder.appendCodePoint('\\'); } if (next == -1) { return null; } builder.appendCodePoint(next); // Add the next character always } } } // Read unquoted string int read; while (!INVALID_UNQUOTED_CHARACTERS.contains(read = reader.read())) { builder.appendCodePoint(read); } // Unread the one extra character that was read; this does nothing if the entire string has been read reader.skip(-1); return builder.isEmpty() ? null : builder.toString(); } @SuppressWarnings("ResultOfMethodCallIgnored") private static int peek(@NotNull StringReader reader) throws IOException { var codePoint = reader.read(); reader.skip(-1); return codePoint; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/ContextGroups.java ================================================ package net.minestom.vanilla.datapack.loot.context; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Entity; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; public class ContextGroups { private static final Map> entityTraits = Set.of( Traits.THIS, Traits.DIRECT_KILLER, Traits.KILLER_ENTITY, Traits.KILLER_PLAYER.map(entity -> (Entity) entity) ).stream() .map(trait -> Map.entry(trait.id(), trait)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); private static final Map> namedTraits = Set.of( Traits.BLOCK_ENTITY.map(block -> (Component) Component.text(block.name())), Traits.DIRECT_KILLER.map(entity -> { @Nullable Component customName = entity.getCustomName(); if (customName != null) return customName; return Component.text(entity.getEntityType().name()); }), Traits.KILLER_ENTITY.map(entity -> { @Nullable Component customName = entity.getCustomName(); if (customName != null) return customName; return Component.text(entity.getEntityType().name()); }), Traits.KILLER_PLAYER.map(entity -> { @Nullable Component customName = entity.getCustomName(); if (customName != null) return customName; return Component.text(entity.getEntityType().name()); }) ).stream() .map(trait -> Map.entry(trait.id(), trait)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); private static final Map> nbtTraits = Set.of( Traits.BLOCK_ENTITY.map(Block::nbt), Traits.THIS.map(entity -> entity.tagHandler().asCompound()), Traits.KILLER_ENTITY.map(entity -> entity.tagHandler().asCompound()), Traits.DIRECT_KILLER.map(entity -> entity.tagHandler().asCompound()), Traits.KILLER_PLAYER.map(entity -> entity.tagHandler().asCompound()) ).stream() .map(trait -> Map.entry(trait.id(), trait)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/LootContext.java ================================================ package net.minestom.vanilla.datapack.loot.context; import com.squareup.moshi.JsonReader; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.function.Function; // Information Source: https://minecraft.fandom.com/wiki/Loot_table#Loot_context_types public interface LootContext extends Traits { // Traits interface Trait { String id(); Function finder(); default Trait map(Function mapper) { return new MappedTraitImpl<>(this, mapper); } static Trait fromJson(JsonReader reader) throws IOException { // traits are always just the string ids String id = reader.nextString(); return Traits.fromId(id); } } @Nullable T get(Trait trait); default T getOrThrow(Trait trait) { T value = get(trait); if (value == null) throw new IllegalStateException("LootContext does not have trait " + trait.id()); return value; } // Not used. Supplies no loot context parameters. // Specifying "type":"empty" means no context parameters can be used in this loot table. record Empty() implements Util.EmptyLootContext { } // Opening of a container with loot table (can be barrel, chest, trapped chest, hopper, minecart with chest, // boat with chest, minecart with hopper, dispenser, dropper, and shulker box). // The command /loot … loot record Chest(Point origin, @Nullable net.minestom.server.entity.Entity entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Chest::origin) .put(THIS, Chest::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Not used for loot table. Specifying "type":"command" doesn't make sense. // Used internally by commands such as /item modify or /execute (if|unless) predicate. record Command(Point origin, @Nullable net.minestom.server.entity.Entity entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Command::origin) .put(THIS, Command::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Not used for loot table. Specifying "type":"selector" doesn't make sense. // Used internally by the predicate target selector argument. record Selector(Point origin, net.minestom.server.entity.Entity entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Selector::origin) .put(THIS, Selector::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Fishing. // The command /loot … fish . record Fishing(Point origin, ItemStack tool, @Nullable net.minestom.server.entity.Entity entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Fishing::origin) .put(TOOL, Fishing::tool) .put(THIS, Fishing::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Loots from a living entity's death. // The command /loot … kill . record Entity(Point origin, net.minestom.server.entity.Entity entity, DamageType damageSource, @Nullable net.minestom.server.entity.Entity killer, @Nullable net.minestom.server.entity.Entity directKiller, @Nullable net.minestom.server.entity.Player killerPlayer) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Entity::origin) .put(THIS, Entity::entity) .put(DAMAGE_SOURCE, Entity::damageSource) .put(KILLER_ENTITY, Entity::killer) .put(DIRECT_KILLER, Entity::directKiller) .put(KILLER_PLAYER, Entity::killerPlayer) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Using a brush on suspicious sand that has a loot table. record Archeology(Point origin, @Nullable net.minestom.server.entity.Player entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Archeology::origin) .put(THIS, Archeology::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Gift from a cat or villager. record Gift(Point origin, net.minestom.server.entity.Player entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, Gift::origin) .put(THIS, Gift::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Bartering with piglins. record Barter(net.minestom.server.entity.Player entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(THIS, Barter::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Loot table set as an advancement's reward. record AdvancementReward(Point origin, net.minestom.server.entity.Player entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, AdvancementReward::origin) .put(THIS, AdvancementReward::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Not used for loot table. Specifying "type":"advancement_entity" doesn't make sense. // Used internally by an advancement invokes a predicate. record AdvancementEntity(Point origin, net.minestom.server.entity.Player entity) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(ORIGIN, AdvancementEntity::origin) .put(THIS, AdvancementEntity::entity) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } // Not used. Supplies all loot context parameters. // Specifying "type":"generic" or omitting it means no checking for context parameters in this loot table when loading the data pack. record Generic() implements Util.EmptyLootContext { } // Loots from breaking a block. // The command /loot … mine . record Block(net.minestom.server.instance.block.Block blockState, Point origin, ItemStack tool, @Nullable net.minestom.server.entity.Player entity, @Nullable net.minestom.server.instance.block.Block blockEntity, @Nullable Double explosionRadius) implements LootContext { private static final Util.LootContextTraitMap traitMap = Util.LootContextTraitMap.builder() .put(BLOCK_STATE, Block::blockState) .put(ORIGIN, Block::origin) .put(TOOL, Block::tool) .put(THIS, Block::entity) .put(BLOCK_ENTITY, Block::blockEntity) .put(EXPLOSION_RADIUS, Block::explosionRadius) .build(); @Override public @Nullable T get(Trait trait) { return traitMap.obtain(this, trait); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/MappedTraitImpl.java ================================================ package net.minestom.vanilla.datapack.loot.context; import org.jetbrains.annotations.Nullable; import java.util.function.Function; public record MappedTraitImpl(LootContext.Trait trait, Function mapper) implements LootContext.Trait { @Override public String id() { return trait.id(); } @Override public Function finder() { return baseValue -> { T value = trait.finder().apply(baseValue); return value == null ? null : mapper.apply(value); }; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/TraitImpl.java ================================================ package net.minestom.vanilla.datapack.loot.context; import org.jetbrains.annotations.Nullable; import java.util.function.Function; public record TraitImpl(String id, Class type) implements LootContext.Trait { @Override public Function finder() { return o -> { if (type.isInstance(o)) { return type.cast(o); } return null; }; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/Traits.java ================================================ package net.minestom.vanilla.datapack.loot.context; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; interface Traits { LootContext.Trait BLOCK_STATE = new TraitImpl<>("block_state", Block.class); LootContext.Trait ORIGIN = new TraitImpl<>("origin", Point.class); LootContext.Trait DAMAGE_SOURCE = new TraitImpl<>("damage_source", DamageType.class); LootContext.Trait THIS = new TraitImpl<>("this", Entity.class); LootContext.Trait KILLER_ENTITY = new TraitImpl<>("killer", Entity.class); LootContext.Trait KILLER_PLAYER = new TraitImpl<>("killer_player", Player.class); LootContext.Trait DIRECT_KILLER = new TraitImpl<>("direct_killer_entity", Entity.class); LootContext.Trait TOOL = new TraitImpl<>("tool", ItemStack.class); LootContext.Trait BLOCK_ENTITY = new TraitImpl<>("block_entity", Block.class); LootContext.Trait EXPLOSION_RADIUS = new TraitImpl<>("explosion_radius", Double.class); static LootContext.Trait fromId(String id) { return switch (id) { case "block_state" -> BLOCK_STATE; case "origin" -> ORIGIN; case "damage_source" -> DAMAGE_SOURCE; case "this" -> THIS; case "killer" -> KILLER_ENTITY; case "killer_player" -> KILLER_PLAYER; case "direct_killer_entity" -> DIRECT_KILLER; case "tool" -> TOOL; case "block_entity" -> BLOCK_ENTITY; case "explosion_radius" -> EXPLOSION_RADIUS; default -> throw new IllegalArgumentException("Unknown trait id: " + id); }; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/context/Util.java ================================================ package net.minestom.vanilla.datapack.loot.context; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; import java.util.function.Function; class Util { public interface EmptyLootContext extends LootContext { @Override default @Nullable T get(Trait trait) { return null; } } public interface LootContextTraitMap { T obtain(C context, LootContext.Trait trait); static Builder builder() { return new BuilderImpl<>(); } interface Builder { Builder put(LootContext.Trait trait, Function value); LootContextTraitMap build(); } } static class BuilderImpl implements LootContextTraitMap.Builder { private final Map> map = new HashMap<>(); @Override public LootContextTraitMap.Builder put(LootContext.Trait trait, Function value) { map.put(trait.id(), value); return this; } @Override public LootContextTraitMap build() { Map> copy = Map.copyOf(this.map); return new LootContextTraitMap<>() { @Override public T obtain(C context, LootContext.Trait trait) { Object baseValue = copy.get(trait.id()).apply(context); return trait.finder().apply(baseValue); } }; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/function/InBuiltLootFunctions.java ================================================ package net.minestom.vanilla.datapack.loot.function; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.*; import net.kyori.adventure.text.Component; import net.minestom.server.MinecraftServer; import net.minestom.server.color.DyeColor; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EquipmentSlotGroup; import net.minestom.server.entity.Player; import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.entity.attribute.AttributeOperation; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.component.AttributeList; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.registry.DynamicRegistry; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.loot.LootTable; import net.minestom.vanilla.datapack.loot.NBTPath; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.datapack.number.NumberProvider; import net.minestom.vanilla.tag.Tags; import net.minestom.vanilla.utils.JavaUtils; import net.minestom.vanilla.utils.MinestomUtils; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.*; import java.util.random.RandomGenerator; @SuppressWarnings("unused") interface InBuiltLootFunctions { // Applies a predefined bonus formula to the count of the item stack. interface ApplyBonus extends LootFunction { @Override default Key function() { return Key.key("minecraft:apply_bonus"); } Key enchantment(); Key formula(); static ApplyBonus fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeMapAdapted(reader, "formula", Map.of( "minecraft:binomial_with_bonus_count", BinomialWithBonusCount.class, "minecraft:ore_drops", OreDrops.class, "minecraft:uniform_bonus_count", UniformBonusCount.class )); } record BinomialWithBonusCount(Key enchantment, Parameters parameters) implements ApplyBonus { @Override public Key formula() { return Key.key("minecraft:binomial_with_bonus_count"); } public record Parameters(int extra, double probability) { } @Override public ItemStack apply(Context context) { int enchantLevel = MinestomUtils.getEnchantLevel(context.itemStack(), enchantment, 0); double n = enchantLevel + parameters().extra(); RandomGenerator random = context.random(); double sum = 0; for (int i = 0; i < n; i++) { if (random.nextDouble() < parameters().probability()) { sum++; } } return context.itemStack().withAmount((int) sum); } } record UniformBonusCount(Key enchantment, Parameters parameters) implements ApplyBonus { @Override public Key formula() { return Key.key("minecraft:uniform_bonus_count"); } public record Parameters(double bonusMultiplier) { } @Override public ItemStack apply(Context context) { int enchantLevel = MinestomUtils.getEnchantLevel(context.itemStack(), enchantment, 0); double n = enchantLevel * parameters.bonusMultiplier(); int count = n == 0.0 ? 0 : (int) context.random().nextDouble(n); return context.itemStack().withAmount(count); } } record OreDrops(Key enchantment) implements ApplyBonus { @Override public Key formula() { return Key.key("minecraft:ore_drops"); } @Override public ItemStack apply(Context context) { int enchantLevel = MinestomUtils.getEnchantLevel(context.itemStack(), enchantment, 0); int itemCount = context.itemStack().amount() * (1 + context.random().nextInt(enchantLevel + 2)); return context.itemStack().withAmount(itemCount); } } } // Copies an entity's or a block entity's name tag into the item's display.Name tag. record CopyName(LootContext.Trait source) implements LootFunction { @Override public Key function() { return Key.key("minecraft:copy_name"); } @Override public ItemStack apply(Context context) { Object entityOrBlockEntity = context.getOrThrow(source); @Nullable Component name = null; if (entityOrBlockEntity instanceof Entity entity) { name = entity.getCustomName(); } if (entityOrBlockEntity instanceof Block blockEntity) { BlockHandler handler = blockEntity.handler(); if (handler == null) return context.itemStack(); // TODO: This is not the correct way to get the block entity's name name = Component.text(handler.getKey().value()); } if (name == null) return context.itemStack(); return context.itemStack().withCustomName(name); } } // Copies NBT values from a specified block entity or entity, or from command storage to the item's tag tag. record CopyNbt(Source source, List operations) implements LootFunction { @Override public Key function() { return Key.key("minecraft:copy_nbt"); } @Override public ItemStack apply(Context context) { BinaryTag sourceNbt = source.nbt(context); BinaryTag itemStackTagNbt = context.itemStack().getTag(Tags.Items.TAG); for (Operation operation : Objects.requireNonNullElse(operations, List.of())) { itemStackTagNbt = operation.applyOperation(sourceNbt, itemStackTagNbt); } return context.itemStack().withTag(Tags.Items.TAG, itemStackTagNbt); } public sealed interface Source { String type(); BinaryTag nbt(LootFunction.Context context); static Source fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> //noinspection unchecked json -> new Context(DatapackLoader.moshi(LootContext.Trait.class).apply(json)); case BEGIN_OBJECT -> json -> JsonUtils.unionStringTypeAdapted(json, "type", type -> switch(type) { case "storage" -> Storage.class; case "context" -> Context.class; default -> null; }); default -> null; }); } record Context(LootContext.Trait target) implements Source { @Override public String type() { return "context"; } @Override public CompoundBinaryTag nbt(LootFunction.Context context) { return context.getOrThrow(target); } } record Storage(Key storageID) implements Source { @Override public String type() { return "storage"; } @Override public CompoundBinaryTag nbt(LootFunction.Context context) { // TODO: Fetch command storage? return CompoundBinaryTag.empty(); } } } public sealed interface Operation { NBTPath.Single source(); NBTPath.Single target(); String op(); BinaryTag apply(BinaryTag source, BinaryTag target); default BinaryTag applyOperation(BinaryTag source, BinaryTag itemStackNbt) { BinaryTag sourceNbt = source().getSingle(source); BinaryTag targetNbt = target().getSingle(itemStackNbt); BinaryTag newNbt = apply(sourceNbt, targetNbt); return target().set(itemStackNbt, newNbt); } static Operation fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "op", key -> switch(key) { case "replace" -> Replace.class; case "merge" -> Merge.class; case "append" -> Append.class; default -> null; }); } record Replace(NBTPath.Single source, NBTPath.Single target) implements Operation { @Override public String op() { return "replace"; } @Override public BinaryTag apply(BinaryTag source, BinaryTag target) { return source; } } record Merge(NBTPath.Single source, NBTPath.Single target) implements Operation { @Override public String op() { return "merge"; } @Override public BinaryTag apply(BinaryTag source, BinaryTag target) { if (!(source instanceof CompoundBinaryTag sourceCompound && target instanceof CompoundBinaryTag targetCompound)) { throw new IllegalArgumentException("Cannot merge non-compound NBT types"); } Map output = new HashMap<>(); for (Map.Entry entry : targetCompound) { output.put(entry.getKey(), entry.getValue()); } for (Map.Entry entry : sourceCompound) { output.put(entry.getKey(), entry.getValue()); } return CompoundBinaryTag.from(output); } } record Append(NBTPath.Single source, NBTPath.Single target) implements Operation { @Override public String op() { return "append"; } @Override public BinaryTag apply(BinaryTag source, BinaryTag target) { if (!(source instanceof ListBinaryTag sourceList && target instanceof ListBinaryTag targetList)) { throw new IllegalArgumentException("Cannot append non-list NBT types"); } BinaryTagType sourceType = sourceList.elementType(); BinaryTagType targetType = targetList.elementType(); if (sourceType != targetType) { throw new IllegalArgumentException("Cannot append lists of different types"); } List values = new ArrayList<>(targetList.stream().toList()); for (BinaryTag nbt : sourceList) { values.add(nbt); } return ListBinaryTag.listBinaryTag(targetType, values); } } } } // Copies block state properties provided by loot context to the item's BlockStateTag tag. record CopyState(Block block, List properties) implements LootFunction { @Override public Key function() { return Key.key("minecraft:copy_state"); } @Override public ItemStack apply(Context context) { Block blockState = context.getOrThrow(LootContext.BLOCK_STATE); Map blockProperties = blockState.properties(); CompoundBinaryTag nbt = context.itemStack().getTag(Tags.Items.BLOCKSTATE); Map propertiesMap = new HashMap<>(); for (String property : properties) { String value = blockProperties.get(property); if (value == null) { throw new IllegalArgumentException("Block " + blockState + " does not have property " + property); } propertiesMap.put(property, StringBinaryTag.stringBinaryTag(value)); } return context.itemStack().withTag(Tags.Items.BLOCKSTATE, CompoundBinaryTag.from(propertiesMap)); } } // Enchants the item with one randomly-selected enchantment. The power of the enchantment, if applicable, is random. // A book will convert to an enchanted book when enchanted. record EnchantRandomly(List enchantments) implements LootFunction { @Override public Key function() { return Key.key("minecraft:enchant_randomly"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.BOOK.equals(itemStack.material())) { itemStack = itemStack.withMaterial(Material.ENCHANTED_BOOK); } EnchantmentList list = itemStack.get(DataComponents.ENCHANTMENTS); for (Enchantment enchantment : enchantments) { var key = MinecraftServer.getEnchantmentRegistry().getKey(enchantment); if (key == null) { throw new IllegalArgumentException("Invalid enchantment: " + enchantment); } // random level 1-3 int level = context.random().nextInt(1, 4); list = list.with(key, level); } return itemStack.with(DataComponents.ENCHANTMENTS, list); } } // Enchants the item, with the specified enchantment level(roughly equivalent to using an enchantment table at that // level). A book will convert to an enchanted book. record EnchantWithLevels(boolean treasure, NumberProvider levels) implements LootFunction { @Override public Key function() { return Key.key("minecraft:enchant_with_levels"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.BOOK.equals(itemStack.material())) { itemStack = itemStack.withMaterial(Material.ENCHANTED_BOOK); } NumberProvider.Context numberContext = context::random; // TODO: Proper enchanting system // int level = levels.asInt().apply(numberContext) / 10; // Enchantment randomEnchant = JavaUtils.randomElement(context.random(), Enchantment.values()); // // return itemStack.withMeta(builder -> builder.enchantment(randomEnchant, (short) level)); return itemStack; } } // If the origin is provided by loot context, converts an empty map into an explorer map leading to a nearby // generated structure. record ExplorationMap(@Nullable String destination, @Nullable String decoration, @Nullable Integer zoom, @Nullable Integer search_radius, @Nullable Boolean skip_existing_chunks) implements LootFunction { @Override public Key function() { return Key.key("minecraft:exploration_map"); } @Override public ItemStack apply(Context context) { String destination = Objects.requireNonNullElse(this.destination, "on_treasure_maps"); String decoration = Objects.requireNonNullElse(this.decoration, "mansion"); int zoom = Objects.requireNonNullElse(this.zoom, 2); int searchRadius = Objects.requireNonNullElse(this.search_radius, 50); boolean skipExistingChunks = Objects.requireNonNullElse(this.skip_existing_chunks, true); ItemStack itemStack = context.itemStack(); if (!Material.MAP.equals(itemStack.material())) { throw new IllegalArgumentException("Item stack must be a map"); } itemStack = itemStack.withMaterial(Material.FILLED_MAP); // TODO: Exploration maps // converts an empty map into an explorer map leading to a nearby generated structure. return itemStack; } } // Removes some items from a stack, if the explosion ratius is provided by loot context. // Each item in the item stack has a chance of 1/explosion radius to be lost. record ExplosionDecay() implements LootFunction { @Override public Key function() { return Key.key("minecraft:explosion_decay"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } double explosionRadius = context.getOrThrow(LootContext.EXPLOSION_RADIUS); int itemStackCount = itemStack.amount(); int removeAmount = 0; for (int i = 0; i < itemStackCount; i++) { if (context.random().nextDouble() < 1 / explosionRadius) { removeAmount++; } } return itemStack.withAmount(itemStackCount - removeAmount); } } // Adds required item tags of a player head. record FillPlayerHead(LootContext.Trait entity) implements LootFunction { @Override public Key function() { return Key.key("minecraft:fill_player_head"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (!Material.PLAYER_HEAD.equals(itemStack.material())) { throw new IllegalArgumentException("Item stack must be a player head"); } Entity entity = context.getOrThrow(this.entity); if (!(entity instanceof Player player)) { throw new IllegalArgumentException("Entity must be a player"); } // TODO: Player head //noinspection UnstableApiUsage // return itemStack.withMeta(PlayerHeadMeta.class, builder -> builder.skullOwner(player.getUuid())); return itemStack; } } // Smelts the item as it would be in a furnace without changing its count. record FurnaceSmelt() implements LootFunction { @Override public Key function() { return Key.key("minecraft:furnace_smelt"); } @Override public ItemStack apply(Context context) { // TODO: Furnace smelting return context.itemStack(); } } // Limits the count of every item stack. record LimitCount(Limit limit) implements LootFunction { @Override public Key function() { return Key.key("minecraft:limit_count"); } @Override public ItemStack apply(Context context) { return limit().limit(context); } public interface Limit { ItemStack limit(LootFunction.Context context); static Limit fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, DatapackLoader.moshi(Constant.class), JsonReader.Token.BEGIN_OBJECT, DatapackLoader.moshi(MinMax.class) )); } record Constant(int limit) implements Limit { @Override public ItemStack limit(LootFunction.Context context) { return context.itemStack().withAmount(amount -> Math.min(amount, limit)); } } record MinMax(@Nullable NumberProvider min, @Nullable NumberProvider max) implements Limit { @Override public ItemStack limit(LootFunction.Context context) { return context.itemStack().withAmount(amount -> { if (min != null) amount = Math.max(amount, min.asInt().apply(context::random)); if (max != null) amount = Math.min(amount, max.asInt().apply(context::random)); return amount; }); } } } } // Adjusts the stack size based on the level of the Looting enchantment on the killer entity provided by loot context. record LootingEnchant(NumberProvider count, @Nullable Integer limit) implements LootFunction { @Override public Key function() { return Key.key("minecraft:looting_enchant"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } Entity killer = context.getOrThrow(LootContext.KILLER_ENTITY); int looting; if (killer instanceof Player player) { ItemStack mainHand = player.getItemInMainHand(); int lootingValue = MinestomUtils.getEnchantLevel(mainHand, Enchantment.LOOTING.key(), 0); if (lootingValue == 0) return itemStack; looting = lootingValue; } else { // TODO: Other entities that hold an item return itemStack; } double additionalMultipler = count.asDouble().apply(context::random); int additional = (int) Math.floor(looting * additionalMultipler); return context.itemStack().withAmount(amount -> amount + additional); } } // Add attribute modifiers to the item. record SetAttributes(List modifiers) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_attributes"); } @Override public ItemStack apply(Context context) { AttributeList list = context.itemStack().get(DataComponents.ATTRIBUTE_MODIFIERS); if (list == null) list = AttributeList.EMPTY; for (AttributeModifier modifier : modifiers()) { list = list.with(modifier.apply(context)); } return context.itemStack().with(DataComponents.ATTRIBUTE_MODIFIERS, list); } public enum Operation { ADDITION("addition"), MULTIPLY_BASE("multiply_base"), MULTIPLY_TOTAL("multiply_total"); private final String id; Operation(String id) { this.id = id; } public String getId() { return id; } public AttributeOperation toMinestom() { return switch (this) { case ADDITION -> AttributeOperation.ADD_VALUE; case MULTIPLY_BASE -> AttributeOperation.ADD_MULTIPLIED_BASE; case MULTIPLY_TOTAL -> AttributeOperation.ADD_MULTIPLIED_TOTAL; }; } } public enum Slot { MAINHAND("mainhand"), OFFHAND("offhand"), FEET("feet"), LEGS("legs"), CHEST("chest"), HEAD("head"); private final String id; Slot(String id) { this.id = id; } public String getId() { return id; } public EquipmentSlotGroup toMinestom() { return switch (this) { case MAINHAND -> EquipmentSlotGroup.MAIN_HAND; case OFFHAND -> EquipmentSlotGroup.OFF_HAND; case FEET -> EquipmentSlotGroup.FEET; case LEGS -> EquipmentSlotGroup.LEGS; case CHEST -> EquipmentSlotGroup.CHEST; case HEAD -> EquipmentSlotGroup.HEAD; }; } } public record AttributeModifier(String name, Key attribute, Operation operation, NumberProvider amount, @Nullable UUID id, List slot) { public AttributeModifier(String name, Key attribute, Operation operation, NumberProvider amount, @Nullable UUID id, Slot slot) { this(name, attribute, operation, amount, id, List.of(slot)); } public AttributeList.Modifier apply(Context context) { UUID uuid = Objects.requireNonNullElseGet(id(), UUID::randomUUID); Attribute attribute = Attribute.fromKey(attribute().key()); AttributeOperation operation = operation().toMinestom(); double amount = amount().asDouble().apply(context::random); net.minestom.server.entity.attribute.AttributeModifier modifier = new net.minestom.server.entity.attribute.AttributeModifier(name(), amount, operation); EquipmentSlotGroup slot = JavaUtils.randomElement(context.random(), slot()).toMinestom(); if (attribute == null) { throw new IllegalArgumentException("Invalid attribute: " + attribute()); } return new AttributeList.Modifier(attribute, modifier, slot); } } } // Adds or replaces banner patterns of a banner. Function successfully adds patterns into NBT tag even if invoked on a non-banner. record SetBannerPattern(List patterns, boolean append) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_banner_pattern"); } public record Pattern(String pattern, String color) { public Tags.Items.Banner.Pattern toPattern() { int color = DyeColor.valueOf(this.color().toUpperCase(Locale.ROOT)).ordinal(); return new Tags.Items.Banner.Pattern(pattern(), color); } } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } List patternsList; if (append) { patternsList = new ArrayList<>(itemStack.getTag(Tags.Items.Banner.PATTERNS)); } else { patternsList = new ArrayList<>(); } for (Pattern pattern : patterns()) { patternsList.add(pattern.toPattern()); } return itemStack.withTag(Tags.Items.Banner.PATTERNS, patternsList); } } // Sets the contents of a container block item to a list of entries. record SetContents(List entries, EntityType type) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_contents"); } @Override public ItemStack apply(Context context) { // TODO: Implement return context.itemStack(); } } // Sets the stack size. record SetCount(NumberProvider count, @Nullable Boolean add) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_count"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } boolean add = Objects.requireNonNullElse(this.add, false); int amount = count.asInt().apply(context::random); if (add) { amount += itemStack.amount(); } else { amount = Math.min(amount, itemStack.material().maxStackSize()); } return itemStack.withAmount(amount); } } // Sets the item's damage value (durability). record SetDamage(NumberProvider damage, @Nullable Boolean add) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_damage"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } boolean add = Objects.requireNonNullElse(this.add, false); int damage; if (add) { damage = Objects.requireNonNullElse(itemStack.get(DataComponents.DAMAGE), 0) + this.damage.asInt().apply(context::random); } else { damage = this.damage.asInt().apply(context::random); } return itemStack.with(DataComponents.DAMAGE, damage); } } // Modifies the item's enchantments. A book will convert to an enchanted book. record SetEnchantments(Map enchantments, @Nullable Boolean add) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_enchantments"); } @Override public ItemStack apply(Context context) { boolean add = Objects.requireNonNullElse(this.add, false); ItemStack itemStack = context.itemStack(); EnchantmentList list = itemStack.get(DataComponents.ENCHANTMENTS); if (list == null) list = EnchantmentList.EMPTY; for (var entry : enchantments.entrySet()) { Enchantment enchantment = entry.getKey(); int count = entry.getValue().asInt().apply(context::random); DynamicRegistry.Key key = MinestomUtils.getEnchantKey(enchantment); if (add) { int previousValue = list.has(key) ? list.level(key) : 0; int newValue = previousValue + count; list = list.with(key, newValue); } else { list = list.with(key, count); } } return itemStack.with(DataComponents.ENCHANTMENTS, list); } } // Sets the item tags for instrument items to a random value from a tag. record SetInstrument(Key options) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_instrument"); } @Override public ItemStack apply(Context context) { // TODO: Implement return context.itemStack(); } } // Sets the loot table for a container block when placed and opened. record SetLootTable(Key name, @Nullable Integer seed, String type) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_loot_table"); } @Override public ItemStack apply(Context context) { // TODO: Implement return context.itemStack(); } } // Adds or changes the item's lore. record SetLore(List lore, String entity, @Nullable Boolean replace) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_lore"); } @Override public ItemStack apply(Context context) { ItemStack itemStack = context.itemStack(); if (Material.AIR.equals(itemStack.material())) { return itemStack; } boolean replace = Objects.requireNonNullElse(this.replace, false); List newLore; if (replace) { newLore = lore(); } else { List lore = itemStack.get(DataComponents.LORE); newLore = lore == null ? new ArrayList<>() : new ArrayList<>(lore); newLore.addAll(lore()); } return itemStack.with(DataComponents.LORE, newLore); } } // Adds or changes the item's custom name. record SetName(Component name, String entity) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_name"); } @Override public ItemStack apply(Context context) { return context.itemStack().withCustomName(name); } } // Adds or changes NBT data of the item. record SetNBT(CompoundBinaryTag nbt) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_nbt"); } @Override public ItemStack apply(Context context) { CompoundBinaryTag currentNbt = context.itemStack().toItemNBT(); CompoundBinaryTag newNbt = CompoundBinaryTag.builder() .put(currentNbt) .put(nbt) .build(); return ItemStack.fromItemNBT(newNbt); } } // Sets the Potion tag of an item. record SetPotion(Key potion) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_potion"); } @Override public ItemStack apply(Context context) { return context.itemStack().withTag(Tags.Items.Potion.POTION, potion); } } // Sets the status effects for suspicious stew. Fails if invoked on an item that is not suspicious stew. record SetStewEffect(List effects) implements LootFunction { @Override public Key function() { return Key.key("minecraft:set_stew_effect"); } public record Effect(String type, NumberProvider duration) { } @Override public ItemStack apply(Context context) { if (!Material.SUSPICIOUS_STEW.equals(context.itemStack().material())) { throw new IllegalArgumentException("Cannot set stew effect on non-stew item"); } // TODO: Implement return context.itemStack(); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/function/InBuiltPredicates.java ================================================ package net.minestom.vanilla.datapack.loot.function; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.registry.DynamicRegistry; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.datapack.number.NumberProvider; import net.minestom.vanilla.datapack.tags.ConditionsFor; import net.minestom.vanilla.utils.MinestomUtils; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; interface InBuiltPredicates { /** * alternative—Evaluates a list of predicates and passes if any one of them passes. Invokable from any context. * • terms: The list of predicates to evaluate. A predicate within this array must be an object. * - A predicate, following this structure recursively. */ record Alternative(List terms) implements Predicate { @Override public String condition() { return "alternative"; } @Override public boolean test(LootContext context) { for (Predicate term : terms) { if (term.test(context)) { return true; } } return false; } } /** * block_state_property—Checks the mined block and its block states. Requires block state provided by loot context, and always fails if not provided. * • block: A block ID. The test fails if the block doesn't match. * • properties: (Optional) A map of block state names to values. Errors if the block doesn't have these properties. * • name: A block state and a exact value. The value is a string. * OR * • name: A block state name and a ranged value to match. * • min: The min value. * • max: The max value. */ record BlockStateProperty(String block, @Nullable Map properties) implements Predicate { @Override public String condition() { return "block_state_property"; } @Override public boolean test(LootContext context) { if (properties == null) return false; Block minedBlock = context.get(LootContext.BLOCK_STATE); if (minedBlock == null) return false; for (var entry : properties.entrySet()) { String key = entry.getKey(); Property property = entry.getValue(); String value = minedBlock.properties().get(key); if (!property.test(minedBlock, value)) return false; } return true; } // if (properties == null) return false; public interface Property { boolean test(Block block, String value); static Property fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.STRING, (JsonUtils.IoFunction) json -> new Value(json.nextString()), JsonReader.Token.BEGIN_OBJECT, DatapackLoader.moshi(Range.class) )); } record Value(String property) implements Property { @Override public boolean test(Block block, String value) { // TODO: Test this return property.equals(value); } } record Range(String min, String max) implements Property { @Override public boolean test(Block block, String value) { // TODO: Implement this somehow... :( return false; } } } } /** * damage_source_properties—Checks properties of damage source. Requires origin and damage source provided by loot context, and always fails if not provided. * • predicate: Predicate applied to the damage source. * - Tags common to all damage types */ record DamageSourceProperties(Map predicate) implements Predicate { @Override public String condition() { return "damage_source_properties"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * entity_properties—Checks properties of an entity. Invokable from any context. * • entity: The entity to check. Specifies an entity from loot context. Can be this, killer, direct_killer, or killer_player. * • predicate: Predicate applied to entity, uses same structure as advancements. * - All possible conditions for entities */ record EntityProperties(LootContext.Trait entity, Map predicate) implements Predicate { @Override public String condition() { return "entity_properties"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * entity_scores—Checks the scoreboard scores of an entity. Requires the specified entity provided by loot context, and always fails if not provided. * • entity: The entity to check. Specifies an entity from loot context. Can be this, killer, direct_killer, or killer_player. * • scores: Scores to check. All specified scores must pass for the condition to pass. * • A score: Key name is the objective while the value specifies a range of score values required for the condition to pass. * + min: A number Provider. Minimum score. * + max: A number Provider. Maximum score. * OR * • A score: Shorthand version of the other syntax above, to check the score against a single number only. Key name is the objective while the value is the required score. */ record EntityScores(LootContext.Trait entity, Map scores) implements Predicate { @Override public String condition() { return "entity_scores"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } public sealed interface Score { boolean test(); static Score fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, DatapackLoader.moshi(Value.class), JsonReader.Token.BEGIN_OBJECT, DatapackLoader.moshi(Range.class) )); } record Value(int value) implements Score { @Override public boolean test() { return false; } } record Range(NumberProvider min, NumberProvider max) implements Score { @Override public boolean test() { return false; } } } } /** * inverted—Inverts another loot table condition. Invokable from any context. * • term: The condition to be negated, following the same structure as outlined here, recursively. */ record Inverted(Predicate term) implements Predicate { @Override public String condition() { return "inverted"; } @Override public boolean test(LootContext context) { return !term.test(context); } } /** * killed_by_player—Checks if there is a killer_player entity provided by loot context. Requires killer_player entity provided by loot context, and always fails if not provided. */ record KilledByPlayer() implements Predicate { @Override public String condition() { return "killed_by_player"; } @Override public boolean test(LootContext context) { return context.get(LootContext.KILLER_PLAYER) != null; } } /** * location_check—Checks the current location against location criteria. Requires origin provided by loot context, and always fails if not provided. * • offsetX - optional offsets to location * • offsetY - optional offsets to location * • offsetZ - optional offsets to location * • predicate: Predicate applied to location, uses same structure as advancements. * - Tags common to all locations */ record LocationCheck(@Optional Integer offsetX, @Optional Integer offsetY, @Optional Integer offsetZ, ConditionsFor.Location predicate) implements Predicate { @Override public String condition() { return "location_check"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * match_tool—Checks tool used to mine the block. Requires tool provided by loot context, and always fails if not provided. * • predicate: Predicate applied to item, uses same structure as advancements. * - All possible conditions for items */ record MatchTool(ConditionsFor.Item predicate) implements Predicate { @Override public String condition() { return "match_tool"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * random_chance—Generates a random number between 0.0 and 1.0, and checks if it is less than a specified value. Invokable from any context. * • chance: Success rate as a number 0.0–1.0. */ record RandomChance(float chance) implements Predicate { @Override public String condition() { return "random_chance"; } @Override public boolean test(LootContext context) { return ThreadLocalRandom.current().nextFloat() < chance; } } /** * random_chance_with_looting—Generates a random number between 0.0 and 1.0, and checks if it is less than a specified value which has been affected by the level of Looting on the killer entity. Requires killer entity provided by loot context, and if not provided, the looting level is regarded as 0. * • chance: Base success rate. * • looting_multiplier: Looting adjustment to the base success rate. Formula is chance + (looting_level * looting_multiplier). */ record RandomChanceWithLooting(float chance, float looting_multiplier) implements Predicate { @Override public String condition() { return "random_chance_with_looting"; } @Override public boolean test(LootContext context) { double random = ThreadLocalRandom.current().nextDouble(); int looting = 0; Player player = context.get(LootContext.KILLER_PLAYER); if (player != null) { EnchantmentList enchants = player.getItemInMainHand().get(DataComponents.ENCHANTMENTS); if (enchants != null && enchants.has(Enchantment.LOOTING)) { looting = enchants.level(Enchantment.LOOTING); } } return random < chance + (looting * looting_multiplier); } } /** * reference—Invokes a predicate file and returns its result. Invokable from any context. * • name: The resource location of the predicate to invoke. A cyclic reference causes a parsing failure. */ record Reference(String name) implements Predicate { @Override public String condition() { return "reference"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * survives_explosion—Returns success with 1 ÷ explosion radius probability. Requires explosion radius provided by loot context, and always success if not provided. */ record SurvivesExplosion() implements Predicate { @Override public String condition() { return "survives_explosion"; } @Override public boolean test(LootContext context) { Double explosionRadius = context.get(LootContext.EXPLOSION_RADIUS); if (explosionRadius == null) return true; return ThreadLocalRandom.current().nextFloat() < 1.0 / explosionRadius; } } /** * table_bonus—Passes with probability picked from a list, indexed by enchantment power. Requires tool provided by loot context. If not provided, the enchantment level is regarded as 0. * • enchantment: Resource location of enchantment. * • chances: List of probabilities for enchantment power, indexed from 0. */ record TableBonus(Key enchantment, List chances) implements Predicate { @Override public String condition() { return "table_bonus"; } @Override public boolean test(LootContext context) { ItemStack item = context.getOrThrow(LootContext.TOOL); EnchantmentList enchants = item.get(DataComponents.ENCHANTMENTS); DynamicRegistry.Key enchantment = MinestomUtils.getEnchantKey(this.enchantment); int level = enchants == null || !enchants.has(enchantment) ? 0 : enchants.level(enchantment); return ThreadLocalRandom.current().nextFloat() < chances.get(level); } } /** * time_check—Compares the current day time (or rather, 24000 * day count + day time) against given values. Invokable from any context. * • value: The time to compare the day time against. * • min: A number Provider. The minimum value. * • max: A number Provider. The maximum value. * OR * • value: Shorthand version of value above, used to check for a single value only. Number providers cannot be used in this shorthand form. * • period: If present, the day time is first reduced modulo the given number before being checked against value. For example, setting this to 24000 causes the checked time to be equal to the current daytime. */ record TimeCheck(Value value, @Nullable Integer period) implements Predicate { @Override public String condition() { return "time_check"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } public sealed interface Value { static Value fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, DatapackLoader.moshi(Single.class), JsonReader.Token.BEGIN_OBJECT, DatapackLoader.moshi(MinMax.class) )); } record MinMax(NumberProvider min, NumberProvider max) implements Value { } record Single(int value) implements Value { } } } /** * value_check—Compares a number against another number or range of numbers. Invokable from any context. * • value: A number Provider. The number to test. * • range: The range of numbers to compare value against. * • min: A number Provider. The minimum value. * • max: A number Provider. The maximum value. * OR * • range: Shorthand version of range above, used to compare value against a single number only. Number providers cannot be used in this shorthand form. */ record ValueCheck(NumberProvider value, Range range) implements Predicate { @Override public String condition() { return "value_check"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } public sealed interface Range { static Range fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, DatapackLoader.moshi(Single.class), JsonReader.Token.BEGIN_OBJECT, DatapackLoader.moshi(MinMax.class) )); } record MinMax(NumberProvider min, NumberProvider max) implements Range { } record Single(int value) implements Range { } } } /** * weather_check—Checks the current game weather. Invokable from any context. * • raining: If true, the condition passes only if it is raining or thundering. * • thundering: If true, the condition passes only if it is thundering. */ record WeatherCheck(boolean raining, boolean thundering) implements Predicate { @Override public String condition() { return "weather_check"; } @Override public boolean test(LootContext context) { // TODO: Implement conditions return false; } } /** * all_of- Evaluates a list of predicates and passes if all of them pass. Invokable from any context. * • terms: The list of predicates to evaluate. A predicate within this array must be an object. */ record AllOf(List terms) implements Predicate { @Override public String condition() { return "all_of"; } @Override public boolean test(LootContext context) { return terms.stream().allMatch(predicate -> predicate.test(context)); } } /** * any_of—Evaluates a list of predicates and passes if any of them pass. Invokable from any context. * • terms: The list of predicates to evaluate. A predicate within this array must be an object. */ record AnyOf(List terms) implements Predicate { @Override public String condition() { return "any_of"; } @Override public boolean test(LootContext context) { return terms.stream().anyMatch(predicate -> predicate.test(context)); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/function/LootFunction.java ================================================ package net.minestom.vanilla.datapack.loot.function; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.loot.context.LootContext; import java.io.IOException; import java.util.Map; import java.util.random.RandomGenerator; // aka ItemFunction, or ItemModifier // https://minecraft.fandom.com/wiki/Item_modifier public interface LootFunction extends InBuiltLootFunctions { /** * @return The function id. */ Key function(); /** * Applies the function to the item stack. * * @param context the function context * @return the modified item stack */ ItemStack apply(Context context); static LootFunction fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeMapAdapted(reader, "function", Map.ofEntries( Map.entry("minecraft:apply_bonus", ApplyBonus.class), Map.entry("minecraft:copy_name", CopyName.class), Map.entry("minecraft:copy_nbt", CopyNbt.class), Map.entry("minecraft:copy_state", CopyState.class), Map.entry("minecraft:enchant_randomly", EnchantRandomly.class), Map.entry("minecraft:enchant_with_levels", EnchantWithLevels.class), Map.entry("minecraft:exploration_map", ExplorationMap.class), Map.entry("minecraft:explosion_decay", ExplosionDecay.class), Map.entry("minecraft:fill_player_head", FillPlayerHead.class), Map.entry("minecraft:furnace_smelt", FurnaceSmelt.class), Map.entry("minecraft:limit_count", LimitCount.class), Map.entry("minecraft:looting_enchant", LootingEnchant.class), Map.entry("minecraft:set_attributes", SetAttributes.class), Map.entry("minecraft:set_banner_pattern", SetBannerPattern.class), Map.entry("minecraft:set_contents", SetContents.class), Map.entry("minecraft:set_count", SetCount.class), Map.entry("minecraft:set_damage", SetDamage.class), Map.entry("minecraft:set_enchantments", SetEnchantments.class), Map.entry("minecraft:set_instrument", SetInstrument.class), Map.entry("minecraft:set_loot_table", SetLootTable.class), Map.entry("minecraft:set_lore", SetLore.class), Map.entry("minecraft:set_name", SetName.class), Map.entry("minecraft:set_nbt", SetNBT.class), Map.entry("minecraft:set_potion", SetPotion.class), Map.entry("minecraft:set_stew_effect", SetStewEffect.class) )); } /** * The context of the function. */ interface Context extends LootContext { /** * The random generator used by the function. * * @return the random generator */ RandomGenerator random(); /** * The item stack to apply the function to. * * @return the previous item stack */ ItemStack itemStack(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/loot/function/Predicate.java ================================================ package net.minestom.vanilla.datapack.loot.function; import com.squareup.moshi.JsonReader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.loot.context.LootContext; import java.io.IOException; public interface Predicate extends InBuiltPredicates { String condition(); boolean test(LootContext context); static Predicate fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "condition", condition -> switch (condition) { case "minecraft:alternative" -> Alternative.class; case "minecraft:block_state_property" -> BlockStateProperty.class; case "minecraft:damage_source_properties" -> DamageSourceProperties.class; case "minecraft:entity_properties" -> EntityProperties.class; case "minecraft:entity_scores" -> EntityScores.class; case "minecraft:inverted" -> Inverted.class; case "minecraft:killed_by_player" -> KilledByPlayer.class; case "minecraft:location_check" -> LocationCheck.class; case "minecraft:match_tool" -> MatchTool.class; case "minecraft:random_chance" -> RandomChance.class; case "minecraft:random_chance_with_looting" -> RandomChanceWithLooting.class; case "minecraft:reference" -> Reference.class; case "minecraft:survives_explosion" -> SurvivesExplosion.class; case "minecraft:table_bonus" -> TableBonus.class; case "minecraft:time_check" -> TimeCheck.class; case "minecraft:value_check" -> ValueCheck.class; case "minecraft:weather_check" -> WeatherCheck.class; case "minecraft:any_of" -> AnyOf.class; case "minecraft:all_of" -> AllOf.class; default -> null; }); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/nbt/NBTUtils.java ================================================ package net.minestom.vanilla.datapack.nbt; import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.ListBinaryTag; import net.kyori.adventure.nbt.TagStringIO; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.StringReader; /** * Contains various NBT-related utilities */ public class NBTUtils { /** * Checks to see if everything in {@code guarantee} is contained in {@code comparison}. The comparison is allowed to * have extra fields that are not contained in the guarantee. * @param guarantee the guarantee that the comparison must have all elements of * @param comparison the comparison, that is being compared against the guarantee. NBT compounds in this parameter, * whether deeper in the tree or not, are allowed to have keys that the guarantee does not - it's * basically compared against a standard. * @param assureListOrder whether to assure list order. When true, lists are directly compared, but when * false, the comparison is checked to see if it contains each item in the guarantee. * @return true if the comparison fits the guarantee, otherwise false */ public static boolean compareNBT(@Nullable BinaryTag guarantee, @Nullable BinaryTag comparison, boolean assureListOrder) { if (guarantee == null) { // If there's no guarantee, it must always pass return true; } if (comparison == null) { // If it's null at this point, we already assured that guarantee is not null, so it must be invalid return false; } if (!guarantee.type().equals(comparison.type())) { // If the types aren't equal it can't fulfill the guarantee anyway return false; } // If the list order is assured, it will be handled with the simple #equals call later in the method if (!assureListOrder && guarantee instanceof ListBinaryTag guaranteeList) { ListBinaryTag comparisonList = ((ListBinaryTag) comparison); if (guaranteeList.size() == 0) { return comparisonList.size() == 0; } for (BinaryTag nbt : guaranteeList) { boolean contains = false; for (BinaryTag compare : comparisonList) { if (compareNBT(nbt, compare, false)) { contains = true; break; } } if (!contains) { return false; } } return true; } if (guarantee instanceof CompoundBinaryTag guaranteeCompound) { CompoundBinaryTag comparisonCompound = ((CompoundBinaryTag) comparison); for (String key : guaranteeCompound.keySet()) { if (!compareNBT(guaranteeCompound.get(key), comparisonCompound.get(key), assureListOrder)) { return false; } } return true; } return guarantee.equals(comparison); } /** * Reads a NBT compound from the provided reader.
* This implementation may be slow, as it may try to parse NBT many times, but this is unavoidable for now. */ public static @Nullable CompoundBinaryTag readCompoundSNBT(@NotNull StringReader reader) throws IOException { if (reader.read() != '{') { return null; } StringBuilder string = new StringBuilder("{"); while (true) { // Since this is a compound we should always read to at least the next closing curly brackets. However, we // can't count brackets and skip to until we think they should be valid because they could be escaped. int next; do { next = reader.read(); if (next == -1) { return null; } string.appendCodePoint(next); } while (next != '}'); try { return TagStringIO.get().asCompound(string.toString()); } catch (IOException ignored) {} } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/number/DoubleNumberProviders.java ================================================ package net.minestom.vanilla.datapack.number; import java.util.random.RandomGenerator; interface DoubleNumberProviders { record Constant(double value) implements NumberProvider.Double { @Override public double apply(NumberProvider.Context context) { return value; } } record Uniform(NumberProvider.Double min, NumberProvider.Double max) implements NumberProvider.Double { @Override public double apply(NumberProvider.Context context) { double min = this.min.apply(context); double max = this.max.apply(context); return context.random().nextDouble(min, max); } } record Binomial(NumberProvider.Int n, NumberProvider.Double p) implements NumberProvider.Double { @Override public double apply(NumberProvider.Context context) { int n = this.n.apply(context); double p = this.p.apply(context); RandomGenerator random = context.random(); double sum = 0; for (int i = 0; i < n; i++) { if (random.nextDouble() < p) { sum++; } } return sum; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/number/IntNumberProviders.java ================================================ package net.minestom.vanilla.datapack.number; import java.util.random.RandomGenerator; interface IntNumberProviders { record Constant(int value) implements NumberProvider.Int { @Override public int apply(NumberProvider.Context context) { return value; } } record Uniform(NumberProvider.Int min, NumberProvider.Int max) implements NumberProvider.Int { @Override public int apply(NumberProvider.Context context) { int min = this.min.apply(context); int max = this.max.apply(context); return context.random().nextInt(min, max); } } record Binomial(NumberProvider.Int n, NumberProvider.Double p) implements NumberProvider.Int { @Override public int apply(NumberProvider.Context context) { int n = this.n.apply(context); double p = this.p.apply(context); RandomGenerator random = context.random(); double sum = 0; for (int i = 0; i < n; i++) { if (random.nextDouble() < p) { sum++; } } return (int) sum; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/number/NumberProvider.java ================================================ package net.minestom.vanilla.datapack.number; import com.squareup.moshi.JsonReader; import net.minestom.vanilla.datapack.json.JsonUtils; import java.io.IOException; import java.util.Map; import java.util.random.RandomGenerator; public interface NumberProvider { Int asInt(); Double asDouble(); interface Context { // TODO: Scoreboard query RandomGenerator random(); } interface Int extends NumberProvider, IntNumberProviders { int apply(Context context); default Double asDouble() { return context -> (double) apply(context); } default Int asInt() { return this; } static NumberProvider.Int fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, json -> constant(reader.nextInt()), JsonReader.Token.BEGIN_OBJECT, json -> JsonUtils.unionStringTypeMapAdapted(json, "type", Map.of( "minecraft:constant", Constant.class, "minecraft:uniform", Uniform.class, "minecraft:binomial", Binomial.class )) )); } static NumberProvider.Int constant(int value) { return new IntNumberProviders.Constant(value); } static NumberProvider.Int uniform(NumberProvider.Int min, NumberProvider.Int max) { return new IntNumberProviders.Uniform(min, max); } static NumberProvider.Int binomial(NumberProvider.Int n, NumberProvider.Double p) { return new IntNumberProviders.Binomial(n, p); } } interface Double extends NumberProvider, DoubleNumberProviders { double apply(Context context); default Int asInt() { return context -> (int) apply(context); } default Double asDouble() { return this; } static NumberProvider.Double fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.NUMBER, json -> constant(reader.nextDouble()), JsonReader.Token.BEGIN_OBJECT, json -> JsonUtils.unionStringTypeMapAdapted(json, "type", Map.of( "minecraft:constant", Constant.class, "minecraft:uniform", Uniform.class, "minecraft:binomial", Binomial.class )) )); } static NumberProvider.Double constant(double value) { return new DoubleNumberProviders.Constant(value); } static NumberProvider.Double uniform(NumberProvider.Double min, NumberProvider.Double max) { return new DoubleNumberProviders.Uniform(min, max); } static NumberProvider.Double binomial(NumberProvider.Int n, NumberProvider.Double p) { return new DoubleNumberProviders.Binomial(n, p); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/recipe/Recipe.java ================================================ package net.minestom.vanilla.datapack.recipe; import com.squareup.moshi.Json; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.item.Material; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Stream; public interface Recipe { @NotNull Key type(); @Nullable String group(); static Recipe fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch(type) { case "minecraft:blasting" -> Blasting.class; case "minecraft:campfire_cooking" -> CampfireCooking.class; case "minecraft:crafting_shaped" -> Shaped.class; case "minecraft:crafting_shapeless" -> Shapeless.class; case "minecraft:crafting_transmute" -> Transmute.class; case "minecraft:crafting_special_armordye" -> Special.ArmorDye.class; case "minecraft:crafting_special_bannerduplicate" -> Special.BannerDuplicate.class; case "minecraft:crafting_special_bookcloning" -> Special.BookCloning.class; case "minecraft:crafting_special_firework_rocket" -> Special.FireworkRocket.class; case "minecraft:crafting_special_firework_star" -> Special.FireworkStar.class; case "minecraft:crafting_special_firework_star_fade" -> Special.FireworkStarFade.class; case "minecraft:crafting_special_mapcloning" -> Special.MapCloning.class; case "minecraft:crafting_special_mapextending" -> Special.MapExtending.class; case "minecraft:crafting_special_repairitem" -> Special.RepairItem.class; case "minecraft:crafting_special_shielddecoration" -> Special.ShieldDecoration.class; case "minecraft:crafting_special_tippedarrow" -> Special.TippedArrow.class; case "minecraft:crafting_special_suspiciousstew" -> Special.SuspiciousStew.class; case "minecraft:crafting_decorated_pot" -> DecoratedPot.class; case "minecraft:smelting" -> Smelting.class; case "minecraft:smithing" -> Smithing.class; case "minecraft:smoking" -> Smoking.class; case "minecraft:stonecutting" -> Stonecutting.class; case "minecraft:smithing_trim" -> SmithingTrim.class; case "minecraft:smithing_transform" -> SmithingTransform.class; default -> null; }); } interface CookingRecipe extends Recipe { @NotNull List ingredient(); @NotNull SingleResult result(); double experience(); @Optional Integer cookingTime(); } interface Ingredient { static Ingredient fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMapMapped(reader, Map.of( JsonReader.Token.BEGIN_ARRAY, json -> { Stream.Builder items = Stream.builder(); json.beginArray(); while (json.peek() != JsonReader.Token.END_ARRAY) { items.add(DatapackLoader.moshi(Single.class).apply(json)); } json.endArray(); return new Multi(items.build().toList()); }, JsonReader.Token.STRING, DatapackLoader.moshi(Single.class), JsonReader.Token.NULL, json -> { json.nextNull(); return new None(); } )); } // single means within an array, not necessarily a singular item interface Single extends Ingredient { static Single fromJson(JsonReader reader) throws IOException { String content = reader.nextString(); boolean isTag = content.startsWith("#"); if (isTag) { return new Tag(Key.key(content.substring(1))); } return new Item(Material.fromKey(content)); } } record Item(Material item) implements Single { } record Tag(Key tag) implements Single { } record None() implements Ingredient { } record Multi(List items) implements Ingredient { } } record Result(Material id, @Optional Integer count) { } record SingleResult(Material id) { } record Blasting(String group, @Optional String category, JsonUtils.SingleOrList ingredient, SingleResult result, double experience, @Optional @Json(name = "cookingtime") Integer cookingTime) implements CookingRecipe { @Override public @NotNull Key type() { return Key.key("minecraft:blasting"); } } record CampfireCooking(String group, JsonUtils.SingleOrList ingredient, SingleResult result, double experience, @Optional @Json(name = "cookingtime") Integer cookingTime) implements CookingRecipe { @Override public @NotNull Key type() { return Key.key("minecraft:campfire_cooking"); } } record Shaped(String group, @Optional String category, List pattern, Map key, Result result) implements Recipe { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_shaped"); } } record Shapeless(String group, @Optional String category, JsonUtils.SingleOrList ingredients, Result result) implements Recipe { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_shapeless"); } } record Transmute(String group, @Optional String category, JsonUtils.SingleOrList input, JsonUtils.SingleOrList material, Result result) implements Recipe { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_transmute"); } } sealed interface Special extends Recipe { record ArmorDye(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_armordye"); } } record BannerDuplicate(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_bannerduplicate"); } } record BookCloning(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_bookcloning"); } } record FireworkRocket(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_firework_rocket"); } } record FireworkStar(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_firework_star"); } } record FireworkStarFade(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_firework_star_fade"); } } record MapCloning(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_mapcloning"); } } record MapExtending(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_mapextending"); } } record RepairItem(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_repairitem"); } } record ShieldDecoration(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_shielddecoration"); } } record TippedArrow(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_tippedarrow"); } } record SuspiciousStew(String group) implements Special { @Override public @NotNull Key type() { return Key.key("minecraft:crafting_special_suspiciousstew"); } } } record DecoratedPot(String group, String category) implements Recipe { @Override public @NotNull Key type() { return Key.key("minecraft:decorated_pot"); } } record Smelting(String group, @Optional String category, JsonUtils.SingleOrList ingredient, SingleResult result, double experience, @Optional @Json(name = "cookingtime") Integer cookingTime) implements CookingRecipe { @Override public @NotNull Key type() { return Key.key("minecraft:smelting"); } } record Smoking(String group, JsonUtils.SingleOrList ingredient, SingleResult result, double experience, @Optional @Json(name = "cookingtime") Integer cookingTime) implements CookingRecipe { @Override public @NotNull Key type() { return Key.key("minecraft:smoking"); } } record Stonecutting(@Nullable String group, JsonUtils.SingleOrList ingredient, Result result) implements Recipe { @Override public @NotNull Key type() { return Key.key("minecraft:stonecutting"); } } interface Smithing extends Recipe { Ingredient.Single template(); Ingredient.Single base(); Ingredient.Single addition(); default @NotNull Key type() { return Key.key("minecraft:smithing"); } } record SmithingTrim(String group, Ingredient.Single base, Ingredient.Single addition, String pattern, Ingredient.Single template) implements Smithing { @Override public @NotNull Key type() { return Key.key("minecraft:smithing_trim"); } } record SmithingTransform(String group, Ingredient.Single base, Ingredient.Single addition, Result result, Ingredient.Single template) implements Smithing { @Override public @NotNull Key type() { return Key.key("minecraft:smithing_transform"); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/tags/ConditionsFor.java ================================================ package net.minestom.vanilla.datapack.tags; public class ConditionsFor { public record Location() { } public record Item() { } public record Entity() { } public record Damage() { } public record DamageTypes() { } public record Distance() { } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/tags/Tag.java ================================================ package net.minestom.vanilla.datapack.tags; import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; public record Tag(String namespace, String value) implements Key { public Tag(String string) { this(string.split(":")[0], string.split(":")[1]); } @Override public @NotNull String asString() { return namespace + ":" + value; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/trims/TrimMaterial.java ================================================ package net.minestom.vanilla.datapack.trims; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.minestom.vanilla.datapack.json.Optional; import java.util.Map; /** * The root object asset_name: A string which will be used in the resource pack. description: A JSON text component used for the tooltip on items. The color #258474 is used here. override_armor_materials: Optional. Map of armor material to override color palette. */ public record TrimMaterial(String asset_name, Component description, @Optional Map override_armor_materials) { } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/trims/TrimPattern.java ================================================ package net.minestom.vanilla.datapack.trims; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.minestom.vanilla.datapack.json.Optional; /** * The root object asset_id: A resource location which will be used in the resource pack. description: A JSON text component used for the tooltip on items. template_item: The item representing this pattern. decal: Optional, defaults to false. If true, the pattern texture will be masked based on the underlying armor. */ public record TrimPattern(Key asset_id, Component description, Key template_item, @Optional Boolean decal) { } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/Biome.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; /** * Represents a biome in the game world. * * @param has_precipitation Determines whether the biome has precipitation or not. * @param temperature Controls gameplay features like grass and foliage color, and a height adjusted temperature. * @param temperature_modifier (optional, defaults to none) Modifies temperature before calculating the height adjusted temperature. * @param downfall Controls grass and foliage color. * @param effects Ambient effects in this biome. * @param carvers The carvers to use. TODO: Carvers * @param features List of generation steps (Can be empty). Usually, there are 11 steps, but any amount is possible. TODO: Features * @param creature_spawn_probability (optional) Higher value results in more creatures spawned in world generation. * @param spawners (Required, but can be empty. If this object doesn't contain a certain category, mobs in this category do not spawn.) Entity spawning settings. * @param spawn_costs (Required, but can be empty. Only mobs listed here use the spawn cost mechanism) See Spawn#Spawn costs for details. */ public record Biome( boolean has_precipitation, float temperature, @Optional TemperatureModifier temperature_modifier, float downfall, Effects effects, CarversList carvers, Object features, @Optional Float creature_spawn_probability, Map> spawners, Map spawn_costs ) { /** * Enumeration of temperature modifiers. */ public enum TemperatureModifier { none, frozen } /** * Represents a sound in the game world. */ public interface Sound { Key type(); /** * Reads a Sound from JSON. * * @param reader The JSON reader. * @return The constructed Sound. * @throws IOException If an IO error occurs. */ static Sound fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> json -> new SoundID(Key.key(json.nextString())); case BEGIN_OBJECT -> json -> JsonUtils.unionStringTypeAdapted(json, "type", type -> switch (type) { case "sound_id" -> SoundID.class; case "range" -> Range.class; default -> null; }); default -> null; }); } /** * Represents a sound with a namespace ID. */ record SoundID(Key value) implements Sound { @Override public Key type() { return Key.key("sound_id"); } } /** * Represents a sound with a range. */ record Range(@Optional Float value) implements Sound { @Override public Key type() { return Key.key("range"); } } } /** * Represents the effects of a biome. */ public record Effects( int fog_color, int sky_color, int water_color, int water_fog_color, @Optional Integer foliage_color, @Optional Integer grass_color, @Optional GrassColorModifier grass_color_modifier, @Optional Particle particle, @Optional Sound ambient_sound, @Optional MoodSound mood_sound, @Optional AdditionsSound additions_sound, @Optional List music ) { /** * Represents a particle effect in the game world. */ public record Particle(float probability, Options options) { /** * Represents options for particle effects. */ public interface Options { /** * Gets the namespaced ID of the particle type. * * @return The namespaced ID of the particle type. */ Key type(); /** * Reads options from JSON. * * @param reader The JSON reader. * @return The constructed Options. * @throws IOException If an IO error occurs. */ static Options fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch (type) { case "minecraft:block" -> Block.class; case "minecraft:block_marker" -> BlockMarker.class; case "minecraft:falling_dust" -> FallingDust.class; case "minecraft:item" -> Item.class; case "minecraft:dust" -> Dust.class; case "minecraft:dust_color_transition" -> DustColorTransition.class; case "minecraft:sculk_charge" -> SculkCharge.class; case "minecraft:vibration" -> Vibration.class; case "minecraft:shriek" -> Shriek.class; default -> Generic.class; }); } /** * Represents a block particle. */ record Block(BlockState value) implements Options { @Override public Key type() { return Key.key("block"); } } /** * Represents a block marker particle. */ record BlockMarker(BlockState value) implements Options { @Override public Key type() { return Key.key("block_marker"); } } /** * Represents a falling dust particle. */ record FallingDust(BlockState value) implements Options { @Override public Key type() { return Key.key("falling_dust"); } } /** * Represents an item particle. */ record Item(Value value) implements Options { /** * Represents the value of an item particle. */ record Value(Key id, int count, CompoundBinaryTag tag) { } @Override public Key type() { return Key.key("item"); } } /** * Represents a dust particle. */ record Dust(List color, float scale) implements Options { @Override public Key type() { return Key.key("dust"); } } /** * Represents a dust color transition particle. */ record DustColorTransition(List fromColor, List toColor, float scale) implements Options { @Override public Key type() { return Key.key("dust_color_transition"); } } /** * Represents a sculk charge particle. */ record SculkCharge(float roll) implements Options { @Override public Key type() { return Key.key("sculk_charge"); } } /** * Represents a vibration particle. */ record Vibration(PositionSource destination, int arrival_in_ticks) implements Options { @Override public Key type() { return Key.key("vibration"); } /** * Represents a position source for the vibration particle. */ interface PositionSource { Key type(); /** * Reads a PositionSource from JSON. * * @param reader The JSON reader. * @return The constructed PositionSource. * @throws IOException If an IO error occurs. */ static PositionSource fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch (type) { case "minecraft:block" -> Block.class; case "minecraft:entity" -> Entity.class; default -> null; }); } /** * Represents a block position for the vibration particle. */ record Block(int x, int y, int z) implements PositionSource { @Override public Key type() { return Key.key("block"); } } /** * Represents an entity position source for the vibration particle. */ record Entity(UUID source_entity, @Optional Float y_offset) implements PositionSource { @Override public Key type() { return Key.key("entity"); } } } } /** * Represents a shriek particle. */ record Shriek(int delay) implements Options { @Override public Key type() { return Key.key("shriek"); } } /** * Represents generic particle options. */ record Generic(Key type) implements Options { } } } /** * Represents mood sound settings for a biome. */ public record MoodSound(Sound sound, int tick_delay, int block_search_extent, double offset) { } /** * Represents additions sound settings for a biome. */ public record AdditionsSound(Sound sound, double tick_chance) { } /** * Represents music settings for a biome. */ public record Music(MusicData data, double weight) { } public record MusicData(Sound sound, int min_delay, int max_delay, boolean replace_current_music) { } } public interface CarversList { // air: carver (referenced by ID or inlined), or carver #tag or list (containing either IDs or inlined objects) (Optional; can be empty) — Carvers for the air cave generation step. List carvers(); static CarversList fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> json -> new Single.Reference(Key.key(json.nextString())); case BEGIN_OBJECT, BEGIN_ARRAY -> json -> { var singleOrList = JsonUtils.SingleOrList.fromJson(CarversList.Single.class, json); if (!singleOrList.isList()) { return new Single.Inlined(singleOrList.asObject().carver()); } return new Multiple(singleOrList.asList()); }; default -> null; }); } interface Single extends CarversList { Carver carver(); default List carvers() { return List.of(carver()); } static Single fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> json -> new Reference(Key.key(json.nextString())); case BEGIN_OBJECT -> json -> new Inlined(DatapackLoader.moshi(Carver.class).apply(json)); default -> null; }); } final class Reference implements Single { private final Key id; private @Nullable Carver carver = null; public Reference(Key id) { this.id = id; DatapackLoader.loading().whenFinished(finisher -> { for (var entry : finisher.datapack().namespacedData().entrySet()) { String namespace = entry.getKey(); var carvers = entry.getValue().world_gen().configured_carver(); for (String file : carvers.files()) { var carver = carvers.file(file); Key carverId = Key.key(namespace, file.substring(0, file.length() - ".json".length())); if (carverId.equals(id)) { Reference.this.carver = carver; return; } } } }); } @Override public Carver carver() { if (carver == null) { if (DatapackLoader.loading().isStatic()) { throw new IllegalStateException("Cannot load carver in a static context"); } throw new IllegalStateException("Carver not loaded yet"); } return carver; } public Key id() { return id; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; var that = (Reference) obj; return Objects.equals(this.id, that.id); } @Override public int hashCode() { return Objects.hash(id); } @Override public String toString() { return "Reference[" + "id=" + id + ']'; } } record Inlined(Carver carver) implements Single { } } record Multiple(List singles) implements CarversList { @Override public List carvers() { return singles.stream().map(CarversList.Single::carver).toList(); } } } /** * The spawner data for a single mob. * @param type The namespaced entity id of the mob. * @param weight How often this mob should spawn, higher values produce more spawns. * @param minCount The minimum count of mobs to spawn in a pack. Must be greater than 0. * @param maxCount The maximum count of mobs to spawn in a pack. Must be greater than 0. And must be not less than minCount. */ public record SpawnerData(Key type, int weight, int minCount, int maxCount) { } public enum MobCategory { monster, creature, ambient, water_creature, underground_water_creature, water_ambient, misc, axolotls } /** * Represents the spawn costs for a mob. * @param energy_budget New mob's maximum potential. * @param charge New mob's charge. */ public record SpawnCost(double energy_budget, double charge) { } /** * Enumeration of grass color modifiers. */ public enum GrassColorModifier { none, dark_forest, swamp } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/BlockState.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.json.JsonUtils; import java.io.IOException; import java.util.Map; import java.util.Objects; public record BlockState(String name, Map properties) { public Block toMinestom() { return Objects.requireNonNull(Block.fromKey(name), () -> "Unknown block: " + name) .withProperties(properties); } public static BlockState fromJson(JsonReader reader) throws IOException { reader.beginObject(); String name = JsonUtils.findProperty(reader.peekJson(), "Name", JsonReader::nextString); Map properties = JsonUtils.findProperty(reader.peekJson(), "Properties", json -> JsonUtils.readObjectToMap(json, JsonReader::nextString)); Objects.requireNonNull(name, "expected a non-null name while passing BlockState."); properties = Objects.requireNonNullElseGet(properties, Map::of); while (reader.peek() != JsonReader.Token.END_OBJECT) { reader.skipName(); reader.skipValue(); } reader.endObject(); return new BlockState(name, properties); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/Carver.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import java.io.IOException; // The root object. // // type: The ID of carver type. // config: Configuration values for the carver. public record Carver(Key type, BaseConfig config) { public static Carver fromJson(JsonReader reader) throws IOException { // carvers are a special case since the config object depends on the type of carver String type; try (var json = reader.peekJson()) { json.beginObject(); type = JsonUtils.findProperty(json, "type", JsonReader::nextString); if (type == null) { throw new IOException("Missing type"); } } var configReader = switch (type) { case "minecraft:cave", "minecraft:nether_cave" -> DatapackLoader.moshi(CaveConfig.class); case "minecraft:canyon" -> DatapackLoader.moshi(CanyonConfig.class); default -> throw new IOException("Unknown carver type: " + type); }; reader.beginObject(); var config = JsonUtils.findProperty(reader, "config", configReader); reader.endObject(); if (config == null) { throw new IOException("Missing config"); } return new Carver(Key.key(type), config); } // cave - Carves a cave. A cave is a long tunnel that sometimes branches. Somtimes one or more tunnels start from a circular void. // nether_cave - Similar to cave, but with a less frequency and wider tunnels. And aquifer doesn't work. The carved blocks below bottom_y + 32.0 are filled with lava. // canyon - Carves a canyon. public interface BaseConfig { // probability: The probability that each chunk attempts to generate carvers. Value between 0 and 1 (both inclusive). float probability(); // y: The height at which this carver attempts to generate. HeightProvider y(); // lava_level: The Y-level below or equal to which the carved areas are filled with lava. Doesn't affect nether_cave (where lava level is always bottom_y + 31). (This field is seemingly ignored and always set to -56 (MC-237017), needs testing) HeightProvider lava_level(); // replaceable: Blocks that can be carved. Can be a block ID, a block tag, or a list of block IDs. JsonUtils.SingleOrList replaceable(); // debug_settings: (optional) Replaces blocks in the carved areas for debugging. // // debug_mode: (optional, defauts to false) Enable debug mode for this carver. // air_state: (optional, defaults to acacia button's default state) Replaces air blocks. // water_state: (optional, defaults to acacia button's default state) Replaces water blocks and then waterlogs these blocks. // lava_state: (optional, defaults to acacia button's default state) Replaces lava blocks. // barrier_state: (optional, defaults to acacia button's default state) Replaces barriers of aquifers. @Optional BaseConfig.DebugSettings debug_settings(); record DebugSettings(@Optional Boolean debug_mode, @Optional Block air_state, @Optional Block water_state, @Optional Block lava_state, @Optional Block barrier_state) { } } public record Config(float probability, HeightProvider y, HeightProvider lava_level, JsonUtils.SingleOrList replaceable, @Optional BaseConfig.DebugSettings debug_settings) implements BaseConfig { } // If carver type is cave or nether_cave, additional fields are as follows: // // yScale: Vertically scales circular voids. // vertical_radius_multiplier: Vertically scales cave tunnels. Doesn't affect the length of tunnels. // floor_level: Value between -1.0 and 1.0 (both inclusive). Change the shape of the cave's horizontal floor. If 0.0, carves the terrain with ellipsoids. If 1.0, carves with upper semi-ellipsoids, resulting in a level floor. public record CaveConfig(float probability, HeightProvider y, HeightProvider lava_level, JsonUtils.SingleOrList replaceable, @Optional BaseConfig.DebugSettings debug_settings, FloatProvider yScale, FloatProvider vertical_radius_multiplier, FloatProvider floor_level) implements BaseConfig { } // If carver type is canyon, additional fields are as follows: // yScale: Vertically scales canyons. // vertical_rotation: Vertical rotation as a canyon extends. // shape: The shape to use for the ravine. public record CanyonConfig(float probability, HeightProvider y, HeightProvider lava_level, JsonUtils.SingleOrList replaceable, @Optional BaseConfig.DebugSettings debug_settings, FloatProvider yScale, FloatProvider vertical_rotation, Shape shape) implements BaseConfig { // distance_factor: Scales the length of canyons. Higher values make canyons longer. // thickness: Scales the breadth and height of canyons. // horizontal_radius_factor: Scales the breadth of canyons. Higher values make canyons wider. // vertical_radius_default_factor: Vertically scales canyons. Higher values make canyons deeper. // vertical_radius_center_factor: Scales the height based on the horizontal distance from the canyon's center, resulting in deeper center. // width_smoothness: Higher values smooth canyon walls on the vertical axis. Must be greater than 0. public record Shape( FloatProvider distance_factor, FloatProvider thickness, FloatProvider horizontal_radius_factor, float vertical_radius_default_factor, float vertical_radius_center_factor, int width_smoothness) { } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/DensityFunction.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.worldgen.math.NumberFunction; import java.io.IOException; public interface DensityFunction extends DensityFunctions, NumberFunction { double compute(Context context); default double minValue() { return -maxValue(); } double maxValue(); static DensityFunction fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case NUMBER -> json -> new Constant(json.nextDouble()); case STRING -> json -> new LazyLoadedDensityFunction(json.nextString(), DatapackLoader.loading()); case BEGIN_OBJECT -> json -> JsonUtils.unionStringTypeAdapted(json, "type", type -> switch (type) { case "minecraft:blend_alpha" -> BlendAlpha.class; case "minecraft:blend_offset" -> BlendOffset.class; case "minecraft:beardifier" -> Beardifier.class; case "minecraft:old_blended_noise" -> OldBlendedNoise.class; case "minecraft:flat_cache" -> FlatCache.class; case "minecraft:interpolated" -> Interpolated.class; case "minecraft:cache_2d" -> Cache2D.class; case "minecraft:cache_once" -> CacheOnce.class; case "minecraft:cache_all_in_cell" -> CacheAllInCell.class; case "minecraft:noise" -> NoiseRoot.class; case "minecraft:end_islands" -> EndIslands.class; case "minecraft:weird_scaled_sampler" -> WeirdScaledSampler.class; case "minecraft:shifted_noise" -> ShiftedNoise.class; case "minecraft:range_choice" -> RangeChoice.class; case "minecraft:shift_a" -> ShiftA.class; case "minecraft:shift_b" -> ShiftB.class; case "minecraft:shift" -> Shift.class; case "minecraft:blend_density" -> BlendDensity.class; case "minecraft:clamp" -> Clamp.class; case "minecraft:abs" -> Abs.class; case "minecraft:square" -> Square.class; case "minecraft:cube" -> Cube.class; case "minecraft:half_negative" -> HalfNegative.class; case "minecraft:quarter_negative" -> QuarterNegative.class; case "minecraft:squeeze" -> Squeeze.class; case "minecraft:add" -> Add.class; case "minecraft:mul" -> Mul.class; case "minecraft:min" -> Min.class; case "minecraft:max" -> Max.class; case "minecraft:spline" -> Spline.class; case "minecraft:constant" -> Constant.class; case "minecraft:y_clamped_gradient" -> YClampedGradient.class; default -> null; }); default -> null; }); } static DensityFunction.Context context(double x, double y, double z) { ContextImpl context = new ContextImpl(); context.x = x; context.y = y; context.z = z; return context; } interface Context { double x(); default int blockX() { return (int) Math.floor(x()); } double y(); default int blockY() { return (int) Math.floor(y()); } double z(); default int blockZ() { return (int) Math.floor(z()); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/DensityFunctions.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.Json; import com.squareup.moshi.JsonReader; import it.unimi.dsi.fastutil.doubles.Double2DoubleFunction; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.worldgen.math.CubicSpline; import net.minestom.vanilla.datapack.worldgen.noise.BlendedNoise; import net.minestom.vanilla.datapack.worldgen.noise.Noise; import net.minestom.vanilla.datapack.worldgen.noise.SimplexNoise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.storage.DoubleStorage; import net.minestom.vanilla.datapack.worldgen.util.Util; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.Objects; import java.util.function.DoubleSupplier; interface DensityFunctions { class ContextImpl implements DensityFunction.Context { public double x; public double y; public double z; @Override public double x() { return x; } @Override public double y() { return y; } @Override public double z() { return z; } } // blend_alpha goes from 0 ("use old terrain") to 1 ("use new terrain") record BlendAlpha() implements DensityFunction { @Override public double compute(Context context) { return 1; } @Override public double maxValue() { return 1; } @Override public double minValue() { return 0; } } // blend_offset and blend_density are the offset and density values to use for the old terrain. record BlendOffset() implements DensityFunction { @Override public double compute(Context context) { return 0; } @Override public double maxValue() { return Double.POSITIVE_INFINITY; } @Override public double minValue() { return Double.NEGATIVE_INFINITY; } } record Beardifier() implements DensityFunction { @Override public double compute(Context context) { return 0; } @Override public double maxValue() { return Double.POSITIVE_INFINITY; } @Override public double minValue() { return Double.NEGATIVE_INFINITY; } } class OldBlendedNoise implements DensityFunction { private final BlendedNoise noise; private OldBlendedNoise(Params params) { this.noise = new BlendedNoise(DatapackLoader.loading().random(), params.xz_scale(), params.y_scale(), params.xz_factor(), params.y_factor(), params.smear_scale_multiplier()); } public record Params(double xz_scale, double y_scale, double xz_factor, double y_factor, double smear_scale_multiplier) { } public static OldBlendedNoise fromJson(JsonReader reader) throws IOException { Params params = DatapackLoader.moshi(Params.class).apply(reader); return new OldBlendedNoise(params); } @Override public double compute(Context context) { return noise.sample(context.x(), context.y(), context.z()); } @Override public double maxValue() { return noise.minValue(); } @Override public double minValue() { return noise.maxValue(); } } interface Wrapped extends DensityFunction { DensityFunction wrapped(); @Override default double minValue() { return wrapped().minValue(); } @Override default double maxValue() { return wrapped().maxValue(); } } class FlatCache implements Wrapped { private final DensityFunction argument; private int lastQuartX = 0; private int lastQuartZ = 0; private double lastValue = 0; public FlatCache(DensityFunction argument) { this.argument = argument; } public double compute(Context context) { int quartX = context.blockX() >> 2; int quartZ = context.blockZ() >> 2; if (this.lastQuartX != quartX || this.lastQuartZ != quartZ) { this.lastValue = this.argument.compute(DensityFunction.context(quartX << 2, 0, quartZ << 2)); this.lastQuartX = quartX; this.lastQuartZ = quartZ; } return this.lastValue; } @Override public DensityFunction wrapped() { return argument; } } class Interpolated implements Wrapped { private final DensityFunction argument; @Json(ignore = true) private @Nullable DoubleStorage cache; public Interpolated(DensityFunction argument) { this.argument = argument; } @Override public DensityFunction wrapped() { return argument(); } private DoubleStorage cache() { if (cache == null) { cache = DoubleStorage.threadLocal(() -> DoubleStorage.from(argument).cache()); } return cache; } @Override public double compute(Context context) { int blockX = context.blockX(); int blockY = context.blockY(); int blockZ = context.blockZ(); int w = 4; int h = 4; double x = ((blockX % w + w) % w) / (double) w; double y = ((blockY % h + h) % h) / (double) h; double z = ((blockZ % w + w) % w) / (double) w; int firstX = Math.floorDiv(blockX, w) * w; int firstY = Math.floorDiv(blockY, h) * h; int firstZ = Math.floorDiv(blockZ, w) * w; DoubleSupplier noise000 = () -> this.computeCorner(firstX, firstY, firstZ); DoubleSupplier noise001 = () -> this.computeCorner(firstX, firstY, firstZ + w); DoubleSupplier noise010 = () -> this.computeCorner(firstX, firstY + h, firstZ); DoubleSupplier noise011 = () -> this.computeCorner(firstX, firstY + h, firstZ + w); DoubleSupplier noise100 = () -> this.computeCorner(firstX + w, firstY, firstZ); DoubleSupplier noise101 = () -> this.computeCorner(firstX + w, firstY, firstZ + w); DoubleSupplier noise110 = () -> this.computeCorner(firstX + w, firstY + h, firstZ); DoubleSupplier noise111 = () -> this.computeCorner(firstX + w, firstY + h, firstZ + w); return Util.lazyLerp3(x, y, z, noise000, noise100, noise010, noise110, noise001, noise101, noise011, noise111); } private double computeCorner(int x, int y, int z) { return cache().obtain(x, y, z); } public DensityFunction argument() { return argument; } } class Cache2D implements Wrapped { // Only computes the input density once per horizonal position. private final DensityFunction argument; @Json(ignore = true) private DoubleStorage cache; public Cache2D(DensityFunction argument) { this.argument = argument; } private DoubleStorage cache() { if (cache == null) { cache = DoubleStorage.threadLocal(() -> DoubleStorage.from(argument).cache2d()); } return cache; } public double compute(Context context) { int blockX = context.blockX(); int blockY = context.blockY(); int blockZ = context.blockZ(); return cache().obtain(blockX, blockY, blockZ); } @Override public DensityFunction wrapped() { return argument; } } class CacheOnce implements Wrapped { private final DensityFunction argument; private int lastHash = 0; private double lastValue = 0; public CacheOnce(DensityFunction argument) { this.argument = argument; } public double compute(Context context) { int blockX = context.blockX(); int blockY = context.blockY(); int blockZ = context.blockZ(); int hash = Objects.hash(blockX, blockY, blockZ); if (this.lastHash != hash) { this.lastValue = this.argument.compute(context); this.lastHash = hash; } return this.lastValue; } @Override public DensityFunction wrapped() { return argument; } } record CacheAllInCell(DensityFunction wrapped) implements Wrapped { // Used by the game onto final_density and should not be referenced in data packs. // TODO: I have no clue what this means or what it should do public double compute(Context context) { // TODO: Implement throw new UnsupportedOperationException("Not implemented"); } } record NoiseRoot(double xz_scale, double y_scale, Noise noise) implements DensityFunction { @Override public double compute(Context context) { return this.noise.sample(context.x() * this.xz_scale(), context.y() * this.y_scale(), context.z() * this.xz_scale()); } @Override public double maxValue() { return this.noise.maxValue(); } @Override public double minValue() { return this.noise.minValue(); } } class EndIslands implements DensityFunction { private final SimplexNoise islandNoise; public EndIslands() { this(0); } public EndIslands(long seed) { WorldgenRandom random = WorldgenRandom.legacy(seed); random.consumeInt(17292); this.islandNoise = new SimplexNoise(random); } private double calculateHeightScale(int x, int z) { int x0 = x / 2; int z0 = z / 2; int x1 = x % 2; int z1 = z % 2; double f = Util.clamp(100.0 - Math.sqrt((x * x + z * z)) * 8.0, -100.0, 80.0); for (int i = -12; i <= 12; ++i) { for (int j = -12; j <= 12; ++j) { double x2 = x0 + i; double z2 = z0 + j; if (x2 * x2 + z2 * z2 > 4096L && this.islandNoise.sample2D(x2, z2) < -0.8999999761581421) { double f1 = (Math.abs((float) x2) * 3439F + Math.abs((float) z2) * 147F) % 13F + 9F; // we need to use floats here to match vanilla's float overflow double x3 = x1 - i * 2; double z3 = z1 - j * 2; double f2 = Util.clamp(100.0 - Math.sqrt(x3 * x3 + z3 * z3) * f1, -100.0, 80.0); f = Math.max(f, f2); } } } return f; } @Override public double compute(Context context) { return (calculateHeightScale(context.blockX() / 8, context.blockZ() / 8) - 8.0) / 128.0; } @Override public double minValue() { return -0.84375; } @Override public double maxValue() { return 0.5625; } } record WeirdScaledSampler(DensityFunction input, RarityValueMapper rarity_value_mapper, Noise noise) implements DensityFunction { public enum RarityValueMapper { type_1(WeirdScaledSampler::rarityValueMapper1, 2), type_2(WeirdScaledSampler::rarityValueMapper2, 3); private final Double2DoubleFunction mapper; private final double maxValue; RarityValueMapper(Double2DoubleFunction mapper, double maxValue) { this.mapper = mapper; this.maxValue = maxValue; } public Double2DoubleFunction mapper() { return mapper; } public double maxValue() { return maxValue; } } @Override public double compute(Context context) { double rarity = rarity_value_mapper().mapper().apply(input.compute(context)); return rarity * Math.abs(this.noise.sample(context.x() / rarity, context.y() / rarity, context.z() / rarity)); } @Override public double minValue() { return 0; } @Override public double maxValue() { return rarity_value_mapper().maxValue(); } private static double rarityValueMapper1(double value) { if (value < -0.5) { return 0.75; } else if (value < 0) { return 1; } else if (value < 0.5) { return 1.5; } else { return 2; } } private static double rarityValueMapper2(double value) { if (value < -0.75) { return 0.5; } else if (value < -0.5) { return 0.75; } else if (value < 0.5) { return 1; } else if (value < 0.75) { return 2; } else { return 3; } } } record Constant(double value) implements DensityFunction { public static final Constant ZERO = new Constant(0); public static Constant ONE = new Constant(1); @Override public double compute(Context context) { return value; } public double minValue() { return value; } public double maxValue() { return value; } } record ShiftedNoise(double xz_scale, double y_scale, DensityFunction shift_x, DensityFunction shift_y, DensityFunction shift_z, Noise noise) implements DensityFunction { public double compute(Context context) { return this.noise.sample( context.x() * this.xz_scale() + this.shift_x.compute(context), context.y() * this.y_scale() + this.shift_y.compute(context), context.z() * this.xz_scale() + this.shift_z.compute(context) ); } @Override public double maxValue() { return noise().maxValue(); } @Override public double minValue() { return noise().minValue(); } } record RangeChoice(DensityFunction input, double min_inclusive, double max_exclusive, DensityFunction when_in_range, DensityFunction when_out_of_range) implements DensityFunction { public double compute(Context context) { return this.input.compute(context) >= this.min_inclusive && this.input.compute(context) < this.max_exclusive ? this.when_in_range.compute(context) : this.when_out_of_range.compute(context); } public double minValue() { return Math.min(this.when_in_range.minValue(), this.when_out_of_range.minValue()); } public double maxValue() { return Math.max(this.when_in_range.maxValue(), this.when_out_of_range.maxValue()); } } record ShiftA(Noise argument) implements DensityFunction { // Samples a noise at (x/4, 0, z/4), then multiplies it by 4. public double compute(Context context) { double shiftedX = context.x() * 0.25; double shiftedZ = context.z() * 0.25; return argument.sample(shiftedX, 0, shiftedZ) * 4.0; } @Override public double minValue() { return argument.minValue() * 4.0; } @Override public double maxValue() { return argument.maxValue() * 4.0; } } record ShiftB(Noise argument) implements DensityFunction { // Samples a noise at (z/4, x/4, 0), then multiplies it by 4. public double compute(Context context) { double shiftedX = context.x() * 0.25; double shiftedZ = context.z() * 0.25; return argument.sample(shiftedZ, shiftedX, 0) * 4.0; } @Override public double minValue() { return argument.minValue() * 4.0; } @Override public double maxValue() { return argument.maxValue() * 4.0; } } record Shift(Noise argument) implements DensityFunction { // Samples a noise at (x/4, y/4, z/4), then multiplies it by 4. @Override public double compute(Context context) { double shiftedX = context.x() * 0.25; double shiftedY = context.y() * 0.25; double shiftedZ = context.z() * 0.25; return argument.sample(shiftedX, shiftedY, shiftedZ) * 4.0; } @Override public double maxValue() { return argument.maxValue() * 4.0; } @Override public double minValue() { return argument.minValue() * 4.0; } } record BlendDensity(DensityFunction argument) implements DensityFunction { @Override public double compute(Context context) { // TODO: Actually implement this. return argument.compute(context); } public double minValue() { return argument.minValue(); } public double maxValue() { return argument.maxValue(); } } record Clamp(double min, double max, DensityFunction input) implements DensityFunction { public double compute(Context context) { double density = this.input.compute(context); return Util.clamp(density, this.min, this.max); } public double minValue() { return this.min; } public double maxValue() { return this.max; } } record Abs(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); return Math.abs(density); } public double minValue() { // the min value may be higher than 0 if the input's range doesn't include 0 if (this.argument.minValue() <= 0 && this.argument.maxValue() >= 0) { return 0; } return Math.min(Math.abs(this.argument.minValue()), Math.abs(this.argument.maxValue())); } public double maxValue() { return Math.max(Math.abs(this.argument.minValue()), Math.abs(this.argument.maxValue())); } } record Square(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); return Util.square(density); } public double minValue() { return Util.square(this.argument.minValue()); } public double maxValue() { return Util.square(this.argument.maxValue()); } } record Cube(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); return Util.cube(density); } public double minValue() { return Util.cube(this.argument.minValue()); } public double maxValue() { return Util.cube(this.argument.maxValue()); } } record HalfNegative(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); return density > 0 ? density : density * 0.5; } public double minValue() { return this.argument.minValue() * 0.5; } public double maxValue() { return this.argument.maxValue() * 0.5; } } record QuarterNegative(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); return density > 0 ? density : density * 0.25; } public double minValue() { return this.argument.minValue() * 0.25; } public double maxValue() { return this.argument.maxValue(); } } record Squeeze(DensityFunction argument) implements DensityFunction { public double compute(Context context) { double density = this.argument.compute(context); double c = Util.clamp(density, -1, 1); return c / 2.0 - c * c * c / 24.0; } public double minValue() { return this.argument.minValue() / 2.0 - this.argument.maxValue() * this.argument.maxValue() * this.argument.maxValue() / 24.0; } public double maxValue() { return this.argument.maxValue() / 2.0 - this.argument.minValue() * this.argument.minValue() * this.argument.minValue() / 24.0; } } record Add(DensityFunction argument1, DensityFunction argument2) implements DensityFunction { @Override public double compute(Context context) { return this.argument1.compute(context) + this.argument2.compute(context); } @Override public double minValue() { return this.argument1.minValue() + this.argument2.minValue(); } @Override public double maxValue() { return this.argument1.maxValue() + this.argument2.maxValue(); } } record Mul(DensityFunction argument1, DensityFunction argument2) implements DensityFunction { @Override public double compute(Context context) { return this.argument1.compute(context) * this.argument2.compute(context); } @Override public double minValue() { return this.argument1.minValue() * this.argument2.minValue(); } @Override public double maxValue() { return this.argument1.maxValue() * this.argument2.maxValue(); } } record Min(DensityFunction argument1, DensityFunction argument2) implements DensityFunction { @Override public double compute(Context context) { return Math.min(this.argument1.compute(context), this.argument2.compute(context)); } @Override public double minValue() { return Math.min(this.argument1.minValue(), this.argument2.minValue()); } @Override public double maxValue() { return Math.min(this.argument1.maxValue(), this.argument2.maxValue()); } } record Max(DensityFunction argument1, DensityFunction argument2) implements DensityFunction { @Override public double compute(Context context) { return Math.max(this.argument1.compute(context), this.argument2.compute(context)); } @Override public double minValue() { return Math.max(this.argument1.minValue(), this.argument2.minValue()); } @Override public double maxValue() { return Math.max(this.argument1.maxValue(), this.argument2.maxValue()); } } record Spline(CubicSpline spline) implements DensityFunction { public double compute(Context context) { return this.spline.compute(context); } public double minValue() { return this.spline.min(); } public double maxValue() { return this.spline.max(); } } record YClampedGradient(double from_y, double to_y, double from_value, double to_value) implements DensityFunction { public double compute(Context context) { return Util.clampedMap(context.y(), this.from_y, this.to_y, this.from_value, this.to_value); } public double minValue() { return Math.min(this.from_value, this.to_value); } public double maxValue() { return Math.max(this.from_value, this.to_value); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/FloatProvider.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.json.JsonUtils; import java.io.IOException; public interface FloatProvider { Key type(); static FloatProvider fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case NUMBER -> json -> new Constant((float) json.nextDouble()); case BEGIN_OBJECT -> json -> JsonUtils.unionStringTypeAdapted(json, "type", type -> switch (type) { case "minecraft:constant" -> Constant.class; case "minecraft:uniform" -> Uniform.class; case "minecraft:clamped_normal" -> ClampedNormal.class; case "minecraft:trapezoid" -> Trapezoid.class; default -> null; }); default -> null; }); } // value: The constant value to use. record Constant(float value) implements FloatProvider { @Override public Key type() { return Key.key("minecraft:constant"); } } // Gives a number between two bounds. // min_inclusive: The minimum possible value (inclusive). // max_exclusive: The maximum possible value (exclusive). Must be larger than min_inclusive. // record Uniform(Value value) implements FloatProvider { public record Value(float min_inclusive, float max_exclusive) {} @Override public Key type() { return Key.key("minecraft:uniform"); } } // Calculated by clamp(normal(mean, deviation), min, max) // // mean: The mean. // deviation: The deviation. // min: The minimum value to clamp to. // max: The maximum value to clamp to. Must be larger than min. record ClampedNormal(Value value) implements FloatProvider { public record Value(float mean, float deviation, float min, float max) {} @Override public Key type() { return Key.key("minecraft:clamped_normal"); } } // min: The minimum value. // max: The maximum value. Must be larger than min. // plateau: The range in the middle of the trapezoid distribution that has a uniform distribution. Must be less than or equal to max - min record Trapezoid(Value value) implements FloatProvider { public record Value(float min, float max, float plateau) {} @Override public Key type() { return Key.key("minecraft:trapezoid"); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/HeightProvider.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.json.Optional; import java.io.IOException; import java.util.List; public sealed interface HeightProvider { Key type(); static HeightProvider fromJson(JsonReader reader) throws IOException { try (var json = reader.peekJson()) { json.beginObject(); if (JsonUtils.findProperty(json, "type", JsonReader::nextString) == null) { return new Constant(VerticalAnchor.fromJson(reader)); } } return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch (type) { case "minecraft:constant" -> Constant.class; case "minecraft:uniform" -> Uniform.class; case "minecraft:biased_to_bottom" -> BiasedToBottom.class; case "minecraft:very_biased_to_bottom" -> VeryBiasedToBottom.class; case "minecraft:biased_to_top" -> BiasedToTop.class; case "minecraft:weighted_list" -> WeightedList.class; default -> null; }); } // value: The vertical anchor to use as constant height. record Constant(VerticalAnchor value) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:constant"); } } // min_inclusive: The vertical anchor to use as minimum height. // max_inclusive: The vertical anchor to use as maximum height. record Uniform(VerticalAnchor min_inclusive, VerticalAnchor max_inclusive) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:uniform"); } } // min_inclusive: The vertical anchor to use as minimum height. // max_inclusive: The vertical anchor to use as maximum height. // inner: (optional, defaults to 1) The inner value. Must be at least 1. record BiasedToBottom(VerticalAnchor min_inclusive, VerticalAnchor max_inclusive, @Optional Integer inner) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:biased_to_bottom"); } } // min_inclusive: The vertical anchor to use as minimum height. // max_inclusive: The vertical anchor to use as maximum height. // inner: (optional, defaults to 1) The inner value. Must be at least 1. record VeryBiasedToBottom(VerticalAnchor min_inclusive, VerticalAnchor max_inclusive, @Optional Integer inner) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:very_biased_to_bottom"); } } // min_inclusive: The vertical anchor to use as minimum height. // max_inclusive: The vertical anchor to use as maximum height. // plateau: (optional, defaults to 0) The length of the range in the middle of the trapezoid distribution that has a uniform distribution. record BiasedToTop(VerticalAnchor min_inclusive, VerticalAnchor max_inclusive, @Optional Integer plateau) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:biased_to_top"); } } // distribution: (Cannot be empty) A random weighted pool of height providers. record WeightedList(List distribution) implements HeightProvider { @Override public Key type() { return Key.key("minecraft:weighted_list"); } // data: A height provider. // weight: The weight of this entry. public record Entry(HeightProvider provider, int weight) { } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/LazyLoadedDensityFunction.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.DatapackUtils; import org.jetbrains.annotations.Nullable; class LazyLoadedDensityFunction implements DensityFunction { private @Nullable DensityFunction densityFunction = null; public LazyLoadedDensityFunction(String id, DatapackLoader.LoadingContext context) { context.whenFinished(finisher -> this.densityFunction = DatapackUtils.findDensityFunction(finisher.datapack(), id) .orElseThrow(() -> new IllegalStateException("Density function " + id + " not found"))); } private DensityFunction densityFunction() { if (densityFunction == null) { throw new IllegalStateException("Density function not loaded yet"); } return densityFunction; } @Override public double compute(Context context) { return densityFunction().compute(context); } @Override public double maxValue() { return densityFunction().maxValue(); } @Override public double minValue() { return densityFunction().minValue(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/NoiseSettings.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.squareup.moshi.JsonReader; import net.kyori.adventure.key.Key; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.Range; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.worldgen.noise.NormalNoise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.util.Util; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.*; public record NoiseSettings( int sea_level, boolean disable_mob_generation, boolean ore_veins_enabled, boolean aquifers_enabled, boolean legacy_random_source, BlockState default_block, BlockState default_fluid, List spawn_target, Noise noise, NoiseRouter noise_router, SurfaceRule surface_rule ) { public static int cellHeight(NoiseSettings settings) { return settings.noise().size_vertical() << 2; } public static int cellWidth(NoiseSettings settings) { return settings.noise().size_horizontal() << 2; } public static double cellCountY(NoiseSettings settings) { return (double) settings.noise().height() / cellHeight(settings); } public static double minCellY(NoiseSettings settings) { return (double) settings.noise().min_y() / cellHeight(settings); } // Noise parameter for biome public record SpawnTarget(Range.Float temperature, Range.Float humidity, Range.Float continentalness, Range.Float erosion, Range.Float weirdness, Range.Float depth, float offset) { } public record Noise(int min_y, int height, int size_horizontal, int size_vertical) { interface SlideSettings { double target(); double size(); double offset(); static SlideSettings fromJson(Object obj) { if (obj instanceof String str) return SlideSettings.fromJson(new Gson().fromJson(str, JsonObject.class)); if (!(obj instanceof JsonObject root)) throw new IllegalStateException("Root is not a JsonObject"); double target = Util.jsonElse(root, "target", 0.0, JsonElement::getAsDouble); double size = Util.jsonElse(root, "size", 0.0, JsonElement::getAsDouble); double offset = Util.jsonElse(root, "offset", 0.0, JsonElement::getAsDouble); return new SlideSettings() { @Override public double target() { return target; } @Override public double size() { return size; } @Override public double offset() { return offset; } }; } static double apply(SlideSettings slide, double density, double y) { if (slide.size() <= 0) return density; double t = (y - slide.offset()) / slide.size(); return Util.clampedLerp(slide.target(), density, t); } } } /** * noise_router: Routes density functions to noise parameters used for world generation. Each field can be an ID of density function or a density function (can be in constant form or object form). * * initial_density_without_jaggedness: Related to the generation of aquifer and surface rule. At a horizonal position, starting from the top of the world, the game searches from top to bottom with the precision of size_vertical*4 blocks. The first Y-level whose noise value greater than 25/64 is used as the initial terrain height for world generation. This height should be generally lower than the actual terrain height (determined by the final density). * final_density: Determines where there is an air or a default block. If positive, returns default block which will can be replaced by the surface_rule. Otherwise, an air where aquifers can generate. * barrier: Affects whether to separate between aquifers and open areas in caves. Larger values leads to higher probability to separate. * fluid_level_floodedness: Affects the probability of generating liquid in an cave for aquifer. The larger value leads to higher probability. The noise value greater than 1.0 is regarded as 1.0, and value less than -1.0 is regarded as -1.0. * fluid_level_spread: Affects the height of the liquid surface at a horizonal position. Smaller value leads to higher probability for lower height. * lava: Affects whether an aquifer here uses lava instead of water. The threshold is 0.3. * vein_toggle: Affects ore vein type and vertical range. If the noise value is greater than 0.0, the vein will be a copper vein. If the noise value is less than or equal to 0.0, the vein will be an iron vein. * vein_ridged: Controls which blocks are part of a vein. If greater than or equal to 0.0, the block will not be part of a vein. If less than 0.0, the block will be either the vein type's stone block, or possibly an ore block. * vein_gap: Affects which blocks in a vein will be ore blocks. If greater than -0.3, and a random number is less than the absolute value of vein_toggle mapped from 0.4 - 0.6 to 0.1 - 0.3, with values outside of this range clamped, an ore block will be placed, with a 2% chance for the ore block to be a raw metal block. Otherwise, the ore type's stone block will be placed. * temperature: The temperature values for biome placement. This field and the following five fields are used for biome placement. * vegetation: The humidity values for biome placement. * continents: The continentalness values for terrain generation and biome placement. * erosion: The erosion values for terrain generation and biome placement. * depth: The depth values for terrain generation and biome placement. * ridges: The weirdness values for terrain generation and biome placement. */ public record NoiseRouter( DensityFunction initial_density_without_jaggedness, DensityFunction final_density, DensityFunction barrier, DensityFunction fluid_level_floodedness, DensityFunction fluid_level_spread, DensityFunction lava, DensityFunction vein_toggle, DensityFunction vein_ridged, DensityFunction vein_gap, DensityFunction temperature, DensityFunction vegetation, DensityFunction continents, DensityFunction erosion, DensityFunction depth, DensityFunction ridges ) { static final Map noiseCache = Collections.synchronizedMap(new HashMap<>()); public static NormalNoise instantiate(WorldgenRandom.Positional random, NormalNoise.Config params) { var randomKey = random.seedKey(); var cacheMapKey = Objects.hash(randomKey[0], randomKey[1]) + "|" + params.hashCode(); return noiseCache.computeIfAbsent(cacheMapKey, k -> new NormalNoise(random.fromSeed(params.hashCode()), params)); } } /** * Surface rule * type: Type of the surface rule, one of: bandlands [sic], block, condition, or sequence. See below of extra fields for each type. * If type is bandlands [sic] (used in badlands), no extra fields. * If type is blocks (places the specified block), extra fields are as follows: * result_state: The block to use. * If type is sequence (attempts to apply surface rules in order, and only the first successful surface rule is applied), extra fields are as follows: * sequence: (Required, but can be empty) List of surface rules. * : A surface rule. * If type is condition (applies surface rules if the condition is met), extra fields are as follows: * if_true: A surface rule condition. * then_run: A surface rule. */ public interface SurfaceRule { Key type(); Pos2Block apply(Context context); interface Context extends VerticalAnchor.Context { Key biome(); int minY(); int maxY(); int blockX(); int blockY(); int blockZ(); WorldgenRandom random(String string); // misc surface details int stoneDepthAbove(); int surfaceDepth(); int waterHeight(); int minSurfaceLevel(); int stoneDepthBelow(); double surfaceSecondary(); } interface Pos2Block { @Nullable Block apply(int x, int y, int z); } static SurfaceRule fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch (type) { case "minecraft:bandlands" -> Bandlands.class; case "minecraft:block" -> Blocks.class; case "minecraft:sequence" -> Sequence.class; case "minecraft:condition" -> Condition.class; default -> null; }); } record Bandlands() implements SurfaceRule { @Override public Key type() { return Key.key("bandlands"); } @Override public Pos2Block apply(Context context) { // TODO: implement throw new UnsupportedOperationException("Not implemented"); } } record Blocks(BlockState result_state) implements SurfaceRule { @Override public Key type() { return Key.key("blocks"); } @Override public Pos2Block apply(Context context) { return (x, y, z) -> result_state().toMinestom(); } } record Sequence(List sequence) implements SurfaceRule { @Override public Key type() { return Key.key("sequence"); } @Override public Pos2Block apply(Context context) { List rulesWithContext = sequence().stream() .map(rule -> rule.apply(context)) .toList(); return (x, y, z) -> { for (Pos2Block rule : rulesWithContext) { Block result = rule.apply(x, y, z); if (result != null) return result; } return null; }; } } record Condition(SurfaceRuleCondition if_true, SurfaceRule then_run) implements SurfaceRule { @Override public Key type() { return Key.key("condition"); } @Override public Pos2Block apply(Context context) { return (x, y, z) -> { if (if_true().test(context)) { return then_run().apply(context).apply(x, y, z); } return null; }; } } /** * Surface rule condition * type: Type of the surface rule, one of: biome, noise_threshold, vertical_gradient, y_above, water, temperature, steep, not, hole, above_preliminary_surface, or stone_depth. See below of extra fields for each type. * If type is biome (test for the biome), extra fields are as follows: * biome_is: (Required, but can be empty) List of biomes that result in true. * : The ID of a biome. * If type is noise_threshold (Success when the noise value at this XZ losction with Y=0 is within the specified closed interval), extra fields are as follows: * noise: The ID of a noise. * min_threshold: Min threshold of the closed interval. * max_threshold: Max threshold of the closed interval. * If type is vertical_gradient (Makes the block fade upwards. Between the specified y-coords is the gradient itself. For example the gradient between bedrock and deepslate, or between deepslate and stone), extra fields are as follows: * random_name: A namespace ID used as the seed of the random. For example, the seed between bedrock and deepslate in the vanilla game is "minecraft:bedrock_floor", and the seed between deepslate and stone is "minecraft:deepslate". * true_at_and_below: Always succcess if the y-coord is at or below this value. * Choices for a vertical anchor (must choose only one of three) * false_at_and_above: Always fails if the y-coord is at or above this value. The y-coords between the two value produces a gradient, and the probability of success in this gradient is (false_at_and_above - Y) / (false_at_and_above - true_at_and_below) * Choices for a vertical anchor (must choose only one of three) * If type is y_above (checks if it is above a XZ plane at the specified Y level. E.g. block whose Y coordinate is 0 is above Y=0 plane), extra fields are as follows: * anchor: Y level. * Choices for a vertical anchor (must choose only one of three) * surface_depth_multiplier: Value between -20 and 20 (both inclusive). How much it is affected by the surface layer thickness. surfaceLayerThickness * surface_depth_multiplier will be added into anchor. * add_stone_depth: Instead of current block's Y-level, checks the value of "current block's Y-level" plus "the number of non-liquid blocks between current block's downward surface and the lowest air block directly above". For example, if block at Y=2 is air, Y=1 is water, and Y=0 is stone, when applied at the stone, the number of non-liquid blocks between current block's downward surface (in this case, Y=0 plane) and the lowest air block directly above (in this case, air at Y=2) is 1 (that is, this stone itself). * If type is water (Check whether the offset height of the current block relative to the liquid surface (the contact surface between air and liquid) above (always a negative integer less than -1) is greater than the specified value. Always success if there's no liquid between them. For example, if there is only one liquid block between current block and the air block above, the value to check is -2), extra fields are as follows: * offset: The offset height relative to the liquid surface (the contact surface between air and liquid) above. If it is set to a value greater than -1, the condition is successful only if there is no liquid between current block and the lowest air block above. If it is set to -1, it works the same with values greater than -1 in terrain generation, and always successful in carver generation. * surface_depth_multiplier: Value between -20 and 20 (both inclusive). How much it is affected by the surface layer thickness. surfaceLayerThickness * surface_depth_multiplier will be added into the offset. * add_stone_depth: Instead of current block's Y-level, checks the value of "current block's Y-level" plus "the number of non-liquid blocks between current block's downward surface and the lowest air block directly above". For example, if block at Y=2 is air, Y=1 is water, and Y=0 is stone, when applied at the stone, the number of non-liquid blocks between current block's downward surface (in this case, Y=0 plane) and the lowest air block directly above (in this case, air at Y=2) is 1 (that is, this stone itself). * If type is temperature (success when the height-adjusted temperature is low enough to snow. The height-adjusted temperature depends on the biome's temperature and temperature_modifier fields and the current Y-level), no extra fields. * If type is steep (checks current position for steep slopes (with height difference of more than 4 blocks) that are back sun (north or east facing)), no extra fields. * If type is not (inverts the condition), extra fields are as follows: * invert: The condition to invert. * Surface rule condition * If type is hole (check whether the surface layer thickness at this horizonal location is less than 0), no extra fields. * If type is above_preliminary_surface (checks whether it is higher than the preliminary surface. The preliminary surface height is the interpolated initial terrain height (determined by initial_density_without_jaggedness) minus 8 and then plus (surfaceLayerThickness - 8)), no extra fields. * If type is stone_depth (checks whether the distance between the current position and the terrain surface or the cave surface is less than or equal to the specified offset value), extra fields are as follows: * offset: The offset value. * add_surface_depth: Whether to be affected by surface layer thickness. If true, the surface layer thickness will be addded into the offset. * secondary_depth_range: How much it is affected by the noise minecraft:surface_secondary. niseValue × secondary_depth_range will be added into the offset. * surface_type: Either floor or ceiling. If ceiling, checks the distance to the upper surface of cave below (technically, it is the distance to the nearest liquid or air block directly below). For example, if where Y=-1 is water, and where Y=0 is stone, when applied to the stone, the distance to the nearest liquid or air block directly below (in this case, the water at Y=-1) is 0. If it isfloor, checks the distance to the terrain surface or the lower surface of cave above (technically, it is the number of non-liquid blocks between current block and the lowest air block directly above. If there is liquid between current block and the air block above, this value may be less than the actual distance to the surface of terrain or cave). For example, where Y=2 is air, Y=1 is water, and Y=0 is stone, when applying this condition at the stone, the number of non-liquid blocks between current block and the lowest air block directly above (in this case, air at Y=2) is 0. */ interface SurfaceRuleCondition { Key type(); boolean test(SurfaceRule.Context context); static SurfaceRuleCondition fromJson(JsonReader reader) throws IOException { return JsonUtils.unionStringTypeAdapted(reader, "type", type -> switch (type) { case "minecraft:biome" -> Biome.class; case "minecraft:noise_threshold" -> NoiseThreshold.class; case "minecraft:vertical_gradient" -> VerticalGradient.class; case "minecraft:y_above" -> YAbove.class; case "minecraft:water" -> Water.class; case "minecraft:temperature" -> Temperature.class; case "minecraft:steep" -> Steep.class; case "minecraft:not" -> Not.class; case "minecraft:hole" -> Hole.class; case "minecraft:above_preliminary_surface" -> AbovePreliminarySurface.class; case "minecraft:stone_depth" -> StoneDepth.class; default -> null; }); } record Biome(List biome_is) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("biome"); } @Override public boolean test(SurfaceRule.Context context) { return biome_is.contains(context.biome()); } } record NoiseThreshold(Key noise, double min_threshold, double max_threshold) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("noise_threshold"); } @Override public boolean test(SurfaceRule.Context context) { // TODO: Implement this throw new UnsupportedOperationException("Not implemented yet"); } } record VerticalGradient(Key random_name, VerticalAnchor true_at_and_below, VerticalAnchor false_at_and_above) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("vertical_gradient"); } @Override public boolean test(SurfaceRule.Context context) { int trueAtAndBelowY = true_at_and_below().apply(context); int falseAtAndAboveY = false_at_and_above().apply(context); if (context.blockY() <= trueAtAndBelowY) { return true; } if (context.blockY() >= falseAtAndAboveY) { return false; } WorldgenRandom random = context.random(random_name().toString()); double chance = Util.map(context.blockY(), trueAtAndBelowY, falseAtAndAboveY, 1, 0); return random.nextFloat() < chance; } } record YAbove(VerticalAnchor anchor, int surface_depth_multiplier, boolean add_stone_depth) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("y_above"); } @Override public boolean test(SurfaceRule.Context context) { int stoneDepth = add_stone_depth() ? context.stoneDepthAbove() : 0; return context.blockY() + stoneDepth >= anchor.apply(context) + context.surfaceDepth() * surface_depth_multiplier(); } } record Water(int offset, int surface_depth_multiplier, boolean add_stone_depth) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("water"); } @Override public boolean test(SurfaceRule.Context context) { if (context.waterHeight() == Integer.MIN_VALUE) { return true; } int stoneDepth = add_stone_depth() ? context.stoneDepthAbove() : 0; return context.blockY() + stoneDepth >= context.waterHeight() + offset() + context.surfaceDepth() * surface_depth_multiplier(); } } record Temperature() implements SurfaceRuleCondition { @Override public Key type() { return Key.key("temperature"); } @Override public boolean test(SurfaceRule.Context context) { // TODO: Implement this throw new UnsupportedOperationException("Not implemented yet"); } } record Steep() implements SurfaceRuleCondition { @Override public Key type() { return Key.key("steep"); } @Override public boolean test(SurfaceRule.Context context) { // TODO: Implement this throw new UnsupportedOperationException("Not implemented yet"); } } record Not(SurfaceRuleCondition invert) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("not"); } @Override public boolean test(SurfaceRule.Context context) { return !invert.test(context); } } record Hole() implements SurfaceRuleCondition { @Override public Key type() { return Key.key("hole"); } @Override public boolean test(SurfaceRule.Context context) { // TODO: Implement this throw new UnsupportedOperationException("Not implemented yet"); } } record AbovePreliminarySurface() implements SurfaceRuleCondition { @Override public Key type() { return Key.key("above_preliminary_surface"); } @Override public boolean test(SurfaceRule.Context context) { return context.blockY() >= context.minSurfaceLevel(); } } record StoneDepth(int offset, boolean add_surface_depth, int secondary_depth_range, SurfaceType surface_type) implements SurfaceRuleCondition { @Override public Key type() { return Key.key("stone_depth"); } @Override public boolean test(SurfaceRule.Context context) { int depth = switch (surface_type()) { case ceiling -> context.stoneDepthBelow(); case floor -> context.stoneDepthAbove(); }; int surfaceDepth = add_surface_depth() ? context.surfaceDepth() : 0; int secondaryDepth = secondary_depth_range() == 0 ? 0 : (int) Util.map(context.surfaceSecondary(), -1, 1, 0, secondary_depth_range()); return depth <= 1 + offset + surfaceDepth + secondaryDepth; } enum SurfaceType { floor, ceiling } } } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/Structure.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.kyori.adventure.nbt.*; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.vanilla.files.ByteArray; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** * DataVersion: Data version of the NBT structure. * author: Name of the player who created this structure. Only exists for structures saved before 1.13. * size: 3 TAG_Int describing the size of the structure. * palette: Set of different block states used in the structure. * A block. * Name: Block ID. * Properties: List of block state properties, with [name] being the name of the block state property. * Name: The block state name and its value. * palettes: Sets of different block states used in the structure, a random palette gets selected based on coordinates. Used in vanilla by shipwrecks. * A set of different block states used in the structure. * A block. * Name: Block ID. * Properties: List of block state properties, with [name] being the name of the block state property. * Name: The block state name and its value. * blocks: List of individual blocks in the structure. * An individual block. * state: Index of the block in the palette. * pos: 3 TAG_Int describing the position of this block. * nbt: NBT of the associated block entity (optional, only present if the block has one). Does not contain x, y, or z fields. See Block entity format. * entities: List of entities in the structure. * An entity. * pos: 3 TAG_Double describing the exact position of the entity. * blockPos: 3 TAG_Int describing the block position of the entity. * nbt: NBT of the entity (required). See entity format. * * @param DataVersion Data version of the NBT structure. * @param author Name of the player who created this structure. Only exists for structures saved before 1.13. * @param size 3 TAG_Int describing the size of the structure. * @param palette Set of different block states used in the structure. * @param palettes Sets of different block states used in the structure, a random palette gets selected based on coordinates. Used in vanilla by shipwrecks. * @param blocks List of individual blocks in the structure. * @param entities List of entities in the structure. */ public record Structure(int DataVersion, @Nullable String author, Point size, @Nullable Set palette, @UnknownNullability Set> palettes, List blocks, List entities) { public static Structure fromInput(ByteArray content) { try { CompoundBinaryTag root = BinaryTagIO.reader().read(content.toStream()); Objects.requireNonNull(root); int DataVersion = root.getInt("DataVersion"); @Nullable String author = root.getString("author"); ListBinaryTag nbt_size = Objects.requireNonNull(root.getList("size")); ListBinaryTag nbt_palette = root.getList("palette"); ListBinaryTag nbt_palettes = root.getList("palettes"); ListBinaryTag nbt_blocks = Objects.requireNonNull(root.getList("blocks")); ListBinaryTag nbt_entities = Objects.requireNonNull(root.getList("entities")); Point size = parsePoint(nbt_size); // Only one of "palette" OR "palettes" is present Set palette = nbt_palette == null ? null : parsePalette(nbt_palette); Set> palettes = nbt_palettes == null ? null : parsePalettes(nbt_palettes); List blocks = parseBlocks(nbt_blocks); List entities = parseEntities(nbt_entities); return new Structure(DataVersion, author, size, palette, palettes, blocks, entities); } catch (IOException e) { throw new RuntimeException(e); } } private static Point parsePoint(ListBinaryTag nbtSize) { return new Vec(((IntBinaryTag) nbtSize.get(0)).value(), ((IntBinaryTag) nbtSize.get(1)).value(), ((IntBinaryTag) nbtSize.get(2)).value()); } private static Point parseDoublePoint(ListBinaryTag nbtSize) { return new Vec(((DoubleBinaryTag) nbtSize.get(0)).value(), ((DoubleBinaryTag) nbtSize.get(1)).value(), ((DoubleBinaryTag) nbtSize.get(2)).value()); } private static BlockState parseBlockState(CompoundBinaryTag block) { String blockId = Objects.requireNonNull(block.getString("Name")); CompoundBinaryTag properties = block.getCompound("Properties"); return new BlockState( blockId, StreamSupport.stream(properties.spliterator(), false) .map(entry -> Map.entry(entry.getKey(), (StringBinaryTag) entry.getValue())) .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().value())) ); } private static Set parsePalette(ListBinaryTag nbtPalette) { return nbtPalette.stream() .map(CompoundBinaryTag.class::cast) .map(Structure::parseBlockState) .collect(Collectors.toSet()); } private static Set> parsePalettes(ListBinaryTag nbtPalettes) { return nbtPalettes.stream() .map(ListBinaryTag.class::cast) .map(palette -> palette.stream() .map(CompoundBinaryTag.class::cast) .map(Structure::parseBlockState) .collect(Collectors.toSet())) .collect(Collectors.toSet()); } private static List parseBlocks(ListBinaryTag nbtBlocks) { return nbtBlocks.stream() .map(CompoundBinaryTag.class::cast) .map(block -> new Block( Objects.requireNonNull(block.getInt("state")), parsePoint(Objects.requireNonNull(block.getList("pos"))), block.get("nbt") )) .collect(Collectors.toList()); } private static List parseEntities(ListBinaryTag nbtEntities) { return nbtEntities.stream() .map(CompoundBinaryTag.class::cast) .map(entity -> new Entity( parseDoublePoint(Objects.requireNonNull(entity.getList("pos"))), parsePoint(Objects.requireNonNull(entity.getList("blockPos"))), Objects.requireNonNull(entity.getCompound("nbt")) )) .collect(Collectors.toList()); } public record Block(int state, Point pos, @Nullable BinaryTag nbt) { } public record Entity(Point pos, Point blockPos, BinaryTag nbt) { } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/VerticalAnchor.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.squareup.moshi.JsonReader; import java.io.IOException; sealed public interface VerticalAnchor { // context here is for optimization potential interface Context { int minY(); int maxY(); } int apply(Context context); static VerticalAnchor fromJson(JsonReader reader) throws IOException { // Vertical anchor is a special case... // thx mojang! reader.beginObject(); String type = reader.nextName(); int value = reader.nextInt(); reader.endObject(); return switch (type) { case "absolute" -> new Absolute(value); case "above_bottom" -> new AboveBottom(value); case "below_top" -> new BelowTop(value); default -> throw new IllegalStateException("Unexpected value: " + type); }; } record Absolute(int value) implements VerticalAnchor { @Override public int apply(Context context) { return value; } } record AboveBottom(int offset) implements VerticalAnchor { @Override public int apply(Context context) { return context.minY() + offset; } } record BelowTop(int offset) implements VerticalAnchor { @Override public int apply(Context context) { return context.maxY() - offset; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/WorldgenContext.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.minestom.server.world.DimensionType; public interface WorldgenContext { static WorldgenContext create(DimensionType dimension) { return () -> dimension; } default int minY() { return dimension().minY(); } default int maxY() { return dimension().maxY(); } DimensionType dimension(); } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/WorldgenRegistries.java ================================================ package net.minestom.vanilla.datapack.worldgen; import it.unimi.dsi.fastutil.doubles.DoubleList; import net.minestom.vanilla.datapack.worldgen.noise.NormalNoise; public class WorldgenRegistries { public static final NormalNoise.Config SURFACE_NOISE = new NormalNoise.Config(-6, DoubleList.of(1, 1, 1)); public static final NormalNoise.Config SURFACE_SECONDARY_NOISE = new NormalNoise.Config(-6, DoubleList.of(1, 1, 0, 1)); } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/biome/BiomeSource.java ================================================ package net.minestom.vanilla.datapack.worldgen.biome; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.worldgen.util.Util; public interface BiomeSource extends BiomeSources { // export interface BiomeSource { // getBiome(x: number, y: number, z: number, climateSampler: Climate.Sampler): Identifier // } Key getBiome(int x, int y, int z, Climate.Sampler climateSampler); // export namespace BiomeSource { // export function fromJson(obj: unknown): BiomeSource { // const root = Json.readObject(obj) ?? {} // const type = Json.readString(root.type)?.replace(/^minecraft:/, '') // switch (type) { // case 'fixed': return FixedBiomeSource.fromJson(obj) // case 'checkerboard': return CheckerboardBiomeSource.fromJson(obj) // case 'multi_noise': return MultiNoiseBiomeSource.fromJson(obj) // case 'the_end': return TheEndBiomeSource.fromJson(obj) // default: return { getBiome: () => Identifier.create('plains') } // } // } // } static BiomeSource checkerBoard(int shift, Key... biomes) { return new CheckerboardBiomeSource(shift, biomes); } static BiomeSource fixed(Key biome) { return new FixedBiomeSource(biome); } static BiomeSource multiNoise(Climate.Parameters parameters) { return new MultiNoiseBiomeSource(parameters); } static BiomeSource theEnd() { return new TheEndBiomeSource(); } static BiomeSource fromJson(Object obj) { JsonObject root = Util.jsonObject(obj); String type = Util.jsonRequire(root, "type", JsonElement::getAsString).replace("^minecraft:", ""); return switch (type) { case "fixed" -> FixedBiomeSource.fromJson(obj); case "checkerboard" -> CheckerboardBiomeSource.fromJson(obj); case "multi_noise" -> MultiNoiseBiomeSource.fromJson(obj); case "the_end" -> TheEndBiomeSource.fromJson(obj); default -> (x, y, z, climateSampler) -> Key.key("plains"); }; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/biome/BiomeSources.java ================================================ package net.minestom.vanilla.datapack.worldgen.biome; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.util.List; import java.util.Map; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; interface BiomeSources { record CheckerboardBiomeSource(int n, int shift, List biomes) implements BiomeSource { public CheckerboardBiomeSource(int shift, Key[] biomes) { this(biomes.length, shift, List.of(biomes)); } public CheckerboardBiomeSource { if (biomes.isEmpty()) { throw new IllegalArgumentException("Cannot create checkerboard biome source without biomes"); } } @Override public Key getBiome(int x, int y, int z, Climate.Sampler climateSampler) { int i = (((x >> this.shift) + (z >> this.shift)) % this.n + this.n) % this.n; return this.biomes.get(i); } public static CheckerboardBiomeSource fromJson(Object obj) { int scale = Util.jsonElse(Util.jsonObject(obj), "scale", 2, JsonElement::getAsInt); Key[] biomes; if (obj instanceof String) { biomes = new Key[]{Key.key((String) obj)}; } else if (obj instanceof JsonElement) { biomes = Util.jsonArray((JsonElement) obj, element -> Key.key(element.getAsString())).toArray(new Key[0]); } else { throw new IllegalArgumentException("Cannot parse biome source from " + obj); } return new CheckerboardBiomeSource(scale + 2, biomes); } } record FixedBiomeSource(Key biome) implements BiomeSource { @Override public Key getBiome(int x, int y, int z, Climate.Sampler climateSampler) { return this.biome; } public static FixedBiomeSource fromJson(Object obj) { JsonObject root = Util.jsonObject(obj); Key biome = Key.key(Util.jsonElse(root, "biome", "plains", JsonElement::getAsString)); return new FixedBiomeSource(biome); } } record MultiNoiseBiomeSource(Climate.Parameters parameters) implements BiomeSource { @Override public Key getBiome(int x, int y, int z, Climate.Sampler climateSampler) { Climate.TargetPoint target = climateSampler.sample(x, y, z); return this.parameters.find(target); } public static MultiNoiseBiomeSource fromJson(Object obj) { JsonObject root = Util.jsonObject(obj); JsonArray biomes = Util.jsonArray(root.get("biomes")); var biomesList = StreamSupport.stream(biomes.spliterator(), false).map(b -> { JsonObject json = Util.jsonObject(b); var biomeName = Key.key(Util.jsonRequire(json, "biome", JsonElement::getAsString)); var parameters = Climate.ParamPoint.fromJson(json.get("parameters")); return Map.entry(biomeName, parameters); }).toList(); Map> parameters = biomesList.stream().collect(Collectors.toMap( Map.Entry::getValue, e -> e::getKey )); return new MultiNoiseBiomeSource(new Climate.Parameters<>(parameters)); } } record TheEndBiomeSource() implements BiomeSource { private static final Key END = Key.key("the_end"); private static final Key HIGHLANDS = Key.key("end_highlands"); private static final Key MIDLANDS = Key.key("end_midlands"); private static final Key ISLANDS = Key.key("small_end_islands"); private static final Key BARRENS = Key.key("end_barrens"); @Override public Key getBiome(int x, int y, int z, Climate.Sampler climateSampler) { int blockX = x << 2; int blockY = y << 2; int blockZ = z << 2; int sectionX = blockX >> 4; int sectionZ = blockZ >> 4; if (sectionX * sectionX + sectionZ * sectionZ <= 4096) { return END; } DensityFunction.Context context = DensityFunction.context((sectionX * 2 + 1) * 8, blockY, (sectionZ * 2 + 1) * 8); double erosion = climateSampler.erosion().compute(context); if (erosion > 0.25) { return HIGHLANDS; } else if (erosion >= -0.0625) { return MIDLANDS; } else if (erosion >= -0.21875) { return BARRENS; } else { return ISLANDS; } } public static TheEndBiomeSource fromJson(Object obj) { return new TheEndBiomeSource(); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/biome/Climate.java ================================================ package net.minestom.vanilla.datapack.worldgen.biome; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; public class Climate { static final int PARAMETER_SPACE = 7; // export function target(temperature: number, humidity: number, continentalness: number, erosion: number, depth: number, weirdness: number) { // return new TargetPoint(temperature, humidity, continentalness, erosion, depth, weirdness) // } public static TargetPoint target(double temperature, double humidity, double continentalness, double erosion, double depth, double weirdness) { return new TargetPoint(temperature, humidity, continentalness, erosion, depth, weirdness); } // export function parameters(temperature: number | Param, humidity: number | Param, continentalness: number | Param, erosion: number | Param, depth: number | Param, weirdness: number | Param, offset: number) { // return new ParamPoint(param(temperature), param(humidity), param(continentalness), param(erosion), param(depth), param(weirdness), offset) // } public static ParamPoint parameters(double temperature, double humidity, double continentalness, double erosion, double depth, double weirdness, double offset) { return new ParamPoint(param(temperature), param(humidity), param(continentalness), param(erosion), param(depth), param(weirdness), offset); } // export function param(value: number | Param, max?: number) { // if (typeof value === 'number') { // return new Param(value, max ?? value) // } // return value // } public static Param param(double min, double max) { return new Param(min, max); } public static Param param(double value) { return new Param(value, value); } public static Param param(Param value) { return value; } public static Param param(Object value) { if (value instanceof Param) { return (Param) value; } if (value instanceof Number) { return new Param(((Number) value).doubleValue(), ((Number) value).doubleValue()); } throw new IllegalArgumentException("Cannot convert " + value + " to Param"); } // export class Param { // constructor( // public readonly min: number, // public readonly max: number, // ) {} public record Param(double min, double max) { // public distance(param: Param | number) { // const diffMax = (typeof param === 'number' ? param : param.min) - this.max // const diffMin = this.min - (typeof param === 'number' ? param : param.max) // if (diffMax > 0) { // return diffMax // } // return Math.max(diffMin, 0) // } public double distance(Param param) { double diffMax = param.min() - this.max(); double diffMin = this.min() - param.max(); if (diffMax > 0) { return diffMax; } return Math.max(diffMin, 0); } // public union(param: Param) { // return new Param( // Math.min(this.min, param.min), // Math.max(this.max, param.max) // ) // } public Param union(Param param) { return new Param( Math.min(this.min(), param.min()), Math.max(this.max(), param.max()) ); } // public static fromJson(obj: unknown) { // if (typeof obj === 'number') return new Param(obj, obj) // const [min, max] = Json.readArray(obj, e => Json.readNumber(e)) ?? [] // return new Param(min ?? 0, max ?? 0) // } public static Param fromJson(Object obj) { if (obj instanceof JsonElement json) { if (json.isJsonPrimitive()) { return fromJson(json.getAsDouble()); } if (json.isJsonArray()) { double[] array = StreamSupport.stream(json.getAsJsonArray().spliterator(), false) .mapToDouble(JsonElement::getAsDouble) .toArray(); return new Param(array[0], array[1]); } } if (obj instanceof Number) { return new Param(((Number) obj).doubleValue(), ((Number) obj).doubleValue()); } throw new IllegalArgumentException("Cannot convert " + obj + " to Param"); } } // export class ParamPoint { // constructor( // public readonly temperature: Param, // public readonly humidity: Param, // public readonly continentalness: Param, // public readonly erosion: Param, // public readonly depth: Param, // public readonly weirdness: Param, // public readonly offset: number, // ) {} public record ParamPoint(Param temperature, Param humidity, Param continentalness, Param erosion, Param depth, Param weirdness, double offset) { public double fittness(ParamPoint point) { return Util.square(this.temperature().distance(point.temperature())) + Util.square(this.humidity().distance(point.humidity())) + Util.square(this.continentalness().distance(point.continentalness())) + Util.square(this.erosion().distance(point.erosion())) + Util.square(this.depth().distance(point.depth())) + Util.square(this.weirdness().distance(point.weirdness())) + Util.square(this.offset() - point.offset()); } // public fittness(point: ParamPoint | TargetPoint) { // return square(this.temperature.distance(point.temperature)) // + square(this.humidity.distance(point.humidity)) // + square(this.continentalness.distance(point.continentalness)) // + square(this.erosion.distance(point.erosion)) // + square(this.depth.distance(point.depth)) // + square(this.weirdness.distance(point.weirdness)) // + square(this.offset - point.offset) // } // public space() { // return [this.temperature, this.humidity, this.continentalness, this.erosion, this.depth, this.weirdness, new Param(this.offset, this.offset)] // } public Param[] space() { return new Param[]{this.temperature(), this.humidity(), this.continentalness(), this.erosion(), this.depth(), this.weirdness(), new Param(this.offset(), this.offset())}; } // public static fromJson(obj: unknown) { // const root = Json.readObject(obj) ?? {} // return new ParamPoint( // Param.fromJson(root.temperature), // Param.fromJson(root.humidity), // Param.fromJson(root.continentalness), // Param.fromJson(root.erosion), // Param.fromJson(root.depth), // Param.fromJson(root.weirdness), // Json.readInt(root.offset) ?? 0, // ) // } public static ParamPoint fromJson(Object obj) { if (obj instanceof JsonElement json) { if (json.isJsonObject()) { JsonObject root = json.getAsJsonObject(); return new ParamPoint( Param.fromJson(root.get("temperature")), Param.fromJson(root.get("humidity")), Param.fromJson(root.get("continentalness")), Param.fromJson(root.get("erosion")), Param.fromJson(root.get("depth")), Param.fromJson(root.get("weirdness")), root.get("offset").getAsInt() ); } } throw new IllegalArgumentException("Cannot convert " + obj + " to ParamPoint"); } } // export class TargetPoint { // constructor( // public readonly temperature: number, // public readonly humidity: number, // public readonly continentalness: number, // public readonly erosion: number, // public readonly depth: number, // public readonly weirdness: number, // ) {} public record TargetPoint(double temperature, double humidity, double continentalness, double erosion, double depth, double weirdness) { // get offset() { // return 0 // } public double offset() { return 0; } // public toArray() { // return [this.temperature, this.humidity, this.continentalness, this.erosion, this.depth, this.weirdness, this.offset] // } public double[] toArray() { return new double[]{this.temperature(), this.humidity(), this.continentalness(), this.erosion(), this.depth(), this.weirdness(), this.offset()}; } } //export class Parameters { // private readonly index: RTree // // constructor(public readonly things: [ParamPoint, () => T][]) { // this.index = new RTree(things) // } // // public find(target: TargetPoint) { // return this.index.search(target, (node, values) => node.distance(values)) // } //} public static class Parameters { private final RTree index; public Parameters(Map> things) { this.index = new RTree<>(things); } public T find(TargetPoint target) { return this.index.search(target, RNode::distance); } } // export class Sampler { // constructor( // public readonly temperature: DensityFunction, // public readonly humidity: DensityFunction, // public readonly continentalness: DensityFunction, // public readonly erosion: DensityFunction, // public readonly depth: DensityFunction, // public readonly weirdness: DensityFunction, // ) {} // // public static fromRouter(router: NoiseRouter) { // return new Climate.Sampler(router.temperature, router.vegetation, router.continents, router.erosion, router.depth, router.ridges) // } // // sample(x: number, y: number, z: number) { // const context = DensityFunction.context(x << 2, y << 2, z << 2) // return Climate.target(this.temperature.compute(context), this.humidity.compute(context), this.continentalness.compute(context), this.erosion.compute(context), this.depth.compute(context), this.weirdness.compute(context)) // } //} public record Sampler(DensityFunction temperature, DensityFunction humidity, DensityFunction continentalness, DensityFunction erosion, DensityFunction depth, DensityFunction weirdness) { public static Sampler fromRouter(NoiseSettings.NoiseRouter router) { return new Sampler(router.temperature(), router.vegetation(), router.continents(), router.erosion(), router.depth(), router.ridges()); } public TargetPoint sample(int x, int y, int z) { DensityFunction.Context context = DensityFunction.context(x << 2, y << 2, z << 2); return Climate.target(this.temperature().compute(context), this.humidity().compute(context), this.continentalness().compute(context), this.erosion().compute(context), this.depth().compute(context), this.weirdness().compute(context)); } } // type DistanceMetric = (node: RNode, values: number[]) => number interface DistanceMetric { double distance(RNode node, double[] values); } //export class RTree { public static class RTree { // public static readonly CHILDREN_PER_NODE = 10 // private readonly root: RNode private static final int CHILDREN_PER_NODE = 10; private final RNode root; // // constructor(points: [ParamPoint, () => T][]) { // if (points.length === 0) { // throw new Error('At least one point is required to build search tree') // } // this.root = RTree.build(points.map(([point, thing]) => new RLeaf(point, thing))) // } public RTree(Map> points) { if (points.isEmpty()) { throw new IllegalArgumentException("At least one point is required to build search tree"); } var pointList = points.entrySet() .stream() .map(entry -> new RLeaf<>(entry.getKey(), entry.getValue())) .map(leaf -> (RNode) leaf) .toList(); this.root = RTree.build(pointList); } // // private static build(nodes: RNode[]): RNode { // if (nodes.length === 1) { // return nodes[0] // } // if (nodes.length <= RTree.CHILDREN_PER_NODE) { // const sortedNodes = nodes // .map(node => { // let key = 0.0 // for (let i = 0; i < PARAMETER_SPACE; i += 1) { // const param = node.space[i] // key += Math.abs((param.min + param.max) / 2.0) // } // return { key, node } // }) // .sort((a, b) => a.key - b.key) // .map(({ node }) => node) // return new RSubTree(sortedNodes) // } // let f = Infinity // let n3 = -1 // let result: RSubTree[] = [] // for (let n2 = 0; n2 < PARAMETER_SPACE; ++n2) { // nodes = RTree.sort(nodes, n2, false) // result = RTree.bucketize(nodes) // let f2 = 0.0 // for (const subTree2 of result) { // f2 += RTree.area(subTree2.space) // } // if (!(f > f2)) continue // f = f2 // n3 = n2 // } // nodes = RTree.sort(nodes, n3, false) // result = RTree.bucketize(nodes) // result = RTree.sort(result, n3, true) // return new RSubTree(result.map(subTree => RTree.build(subTree.children))) // } private static RNode build(List> nodes) { if (nodes.size() == 1) { return nodes.get(0); } if (nodes.size() <= RTree.CHILDREN_PER_NODE) { List> sortedNodes = nodes.stream() .map(node -> { double key = 0.0; for (int i = 0; i < PARAMETER_SPACE; i += 1) { Param param = node.space[i]; key += Math.abs((param.min() + param.max()) / 2.0); } return Map.entry(key, node); }) .sorted(Comparator.comparingDouble(Map.Entry::getKey)) .map(Map.Entry::getValue) .collect(Collectors.toList()); return new RSubTree<>(sortedNodes); } double f = Double.POSITIVE_INFINITY; int n3 = -1; List> result = new ArrayList<>(); for (int n2 = 0; n2 < PARAMETER_SPACE; ++n2) { nodes = RTree.sort(nodes, n2, false); result = RTree.bucketize(nodes); double f2 = 0.0; for (RSubTree subTree2 : result) { f2 += RTree.area(subTree2.space()); } if (!(f > f2)) continue; f = f2; n3 = n2; } nodes = RTree.sort(nodes, n3, false); result = RTree.bucketize(nodes); result = RTree.sort(result, n3, true); return new RSubTree<>(result.stream().map(subTree -> RTree.build(subTree.children)).collect(Collectors.toList())); } // // private static sort>(nodes: N[], i: number, abs: boolean) { // return nodes // .map(node => { // const param = node.space[i] // const f = (param.min + param.max) / 2 // const key = abs ? Math.abs(f) : f // return { key, node } // }) // .sort((a, b) => a.key - b.key) // .map(({ node }) => node) // } private static > List sort(List nodes, int i, boolean abs) { return nodes.stream() .map(node -> { Param param = node.space().get(i); double f = (param.min() + param.max()) / 2; double key = abs ? Math.abs(f) : f; return Map.entry(key, node); }) .sorted(Comparator.comparingDouble(Map.Entry::getKey)) .map(Map.Entry::getValue) .collect(Collectors.toList()); } // // private static bucketize(nodes: RNode[]) { // const arrayList: RSubTree[] = [] // let arrayList2: RNode[] = [] // const n = Math.pow(10.0, Math.floor(Math.log(nodes.length - 0.01) / Math.log(10.0))) // for (const node of nodes) { // arrayList2.push(node) // if (arrayList2.length < n) continue // arrayList.push(new RSubTree(arrayList2)) // arrayList2 = [] // } // if (arrayList2.length !== 0) { // arrayList.push(new RSubTree(arrayList2)) // } // return arrayList // } private static List> bucketize(List> nodes) { List> arrayList = new ArrayList<>(); List> arrayList2 = new ArrayList<>(); double n = Math.pow(10.0, Math.floor(Math.log(nodes.size() - 0.01) / Math.log(10.0))); for (RNode node : nodes) { arrayList2.add(node); if (arrayList2.size() < n) continue; arrayList.add(new RSubTree<>(arrayList2)); arrayList2 = new ArrayList<>(); } if (!arrayList2.isEmpty()) { arrayList.add(new RSubTree<>(arrayList2)); } return arrayList; } // // private static area(params: Param[]) { // let f = 0.0 // for (const param of params) { // f += Math.abs(param.max - param.min) // } // return f // } private static double area(Collection params) { double f = 0.0; for (Param param : params) { f += Math.abs(param.max() - param.min()); } return f; } // // public search(target: TargetPoint, distance: DistanceMetric) { // const leaf = this.root.search(target.toArray(), distance) // return leaf.thing() // } public T search(TargetPoint target, DistanceMetric distance) { RLeaf leaf = this.root.search(target.toArray(), distance); return leaf.thing.get(); } //} } // export abstract class RNode { // constructor(public readonly space: Param[]) {} // // public abstract search(values: number[], distance: DistanceMetric): RLeaf // // public distance(values: number[]) { // let result = 0 // for (let i = 0; i < PARAMETER_SPACE; i += 1) { // result += square(this.space[i].distance(values[i])) // } // return result // } //} static abstract class RNode { protected final Param[] space; public RNode(Param[] space) { this.space = space; } public abstract RLeaf search(double[] values, DistanceMetric distance); public double distance(double[] values) { double result = 0; for (int i = 0; i < PARAMETER_SPACE; i += 1) { result += Util.square(this.space[i].distance(Param.fromJson(values[i]))); } return result; } public List space() { return Arrays.asList(this.space); } } //export class RSubTree extends RNode { // constructor(public readonly children: RNode[]) { // super(RSubTree.buildSpace(children)) // } // // private static buildSpace(nodes: RNode[]): Param[] { // let space = [...Array(PARAMETER_SPACE)].map(() => new Param(Infinity, -Infinity)) // for (const node of nodes) { // space = [...Array(PARAMETER_SPACE)].map((_, i) => space[i].union(node.space[i])) // } // return space // } // // search(values: number[], distance: DistanceMetric): RLeaf { // let dist = Infinity // let leaf: RLeaf | null = null // for (const node of this.children) { // const d1 = distance(node, values) // if (dist <= d1) continue // const leaf2 = node.search(values, distance) // const d2 = node == leaf2 ? d1 : distance(leaf2, values) // if (dist <= d2) continue // dist = d2 // leaf = leaf2 // } // return leaf! // } //} static class RSubTree extends RNode { private final List> children; public RSubTree(List> children) { super(RSubTree.buildSpace(children)); this.children = children; } private static Param[] buildSpace(List> nodes) { Param[] space = new Param[PARAMETER_SPACE]; for (int i = 0; i < PARAMETER_SPACE; i += 1) { space[i] = new Param(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); } for (RNode node : nodes) { for (int i = 0; i < PARAMETER_SPACE; i += 1) { space[i] = space[i].union(node.space().get(i)); } } return space; } @Override public RLeaf search(double[] values, DistanceMetric distance) { double dist = Double.POSITIVE_INFINITY; RLeaf leaf = null; for (RNode node : this.children) { double d1 = distance.distance(node, values); if (dist <= d1) continue; RLeaf leaf2 = node.search(values, distance); double d2 = node == leaf2 ? d1 : distance.distance(leaf2, values); if (dist <= d2) continue; dist = d2; leaf = leaf2; } return leaf; } } // export class RLeaf extends RNode { // constructor(point: ParamPoint, public readonly thing: () => T) { // super(point.space()) // } // // search() { // return this // } //} static class RLeaf extends RNode { private final Supplier thing; public RLeaf(ParamPoint point, Supplier thing) { super(point.space()); this.thing = thing; } @Override public RLeaf search(double[] values, DistanceMetric distance) { return this; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/math/CubicSpline.java ================================================ package net.minestom.vanilla.datapack.worldgen.math; import com.squareup.moshi.JsonReader; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.WeakHashMap; public interface CubicSpline extends NumberFunction { static CubicSpline fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case NUMBER -> json -> new Constant(json.nextDouble()); case BEGIN_OBJECT -> json -> DatapackLoader.moshi(MultiPoint.class).apply(json); default -> null; }); } double min(); double max(); record Constant(double value) implements CubicSpline { @Override public double min() { return value; } @Override public double max() { return value; } @Override public double compute(DensityFunction.Context context) { return value; } } record MultiPoint(DensityFunction coordinate, List points) implements CubicSpline { // TODO: Remove this static map, and instead use lazily loaded class fields private static final Map cache = Collections.synchronizedMap(new WeakHashMap<>()); private record CachedMinMax(double min, double max) { } @Override public double compute(DensityFunction.Context coordinate) { double c = this.coordinate.compute(coordinate); int pointsLength = this.points.size(); int i = Util.binarySearch(0, pointsLength, n -> c < this.points.get(n).location() - 1); int n = pointsLength - 1; if (i < 0) { Point point = this.points.get(0); return point.value().compute(coordinate) + point.derivative() * (c - point.location()); } if (i == n) { Point point = this.points.get(n); return point.value().compute(coordinate) + point.derivative() * (c - point.location()); } if (i > n) { throw new IllegalStateException("i > n"); } Point point0 = this.points.get(i); Point point1 = this.points.get(i + 1); double loc0 = point0.location(); double loc1 = point1.location(); double der0 = point0.derivative(); double der1 = point1.derivative(); double f = (c - loc0) / (loc1 - loc0); double val0 = point0.value().compute(coordinate); double val1 = point1.value().compute(coordinate); double f8 = der0 * (loc1 - loc0) - (val1 - val0); double f9 = -der1 * (loc1 - loc0) + (val1 - val0); return Util.lerp(f, val0, val1) + f * (1.0 - f) * Util.lerp(f, f8, f9); } private CachedMinMax minMax() { if (cache.containsKey(this)) { return cache.get(this); } int pointsLength = this.points.size(); int lastIdx = pointsLength - 1; double splineMin = Double.POSITIVE_INFINITY; double splineMax = Double.NEGATIVE_INFINITY; double coordinateMin = coordinate.minValue(); double coordinateMax = coordinate.maxValue(); Point first = this.points.get(0); if (coordinateMin < first.location()) { double minExtend = MultiPoint.linearExtend(coordinateMin, first, first.value().min()); double maxExtend = MultiPoint.linearExtend(coordinateMin, first, first.value().max()); splineMin = Math.min(splineMin, Math.min(minExtend, maxExtend)); splineMax = Math.max(splineMax, Math.max(minExtend, maxExtend)); } Point last = this.points.get(lastIdx); if (coordinateMax > last.location()) { double minExtend = MultiPoint.linearExtend(coordinateMax, last, last.value().min()); double maxExtend = MultiPoint.linearExtend(coordinateMax, last, last.value().max()); splineMin = Math.min(splineMin, Math.min(minExtend, maxExtend)); splineMax = Math.max(splineMax, Math.max(minExtend, maxExtend)); } for (Point point : points()) { CubicSpline innerSpline = point.value(); splineMin = Math.min(splineMin, innerSpline.min()); splineMax = Math.max(splineMax, innerSpline.max()); } for (int i = 0; i < lastIdx; ++i) { Point pointLeft = this.points.get(i); Point pointRight = this.points.get(i + 1); double locationLeft = pointLeft.location(); double locationRight = pointRight.location(); double locationDelta = locationRight - locationLeft; CubicSpline splineLeft = pointLeft.value(); CubicSpline splineRight = pointRight.value(); double minLeft = splineLeft.min(); double maxLeft = splineLeft.max(); double minRight = splineRight.min(); double maxRight = splineRight.max(); double derivativeLeft = pointLeft.derivative(); double derivativeRight = pointRight.derivative(); if (derivativeLeft != 0.0 || derivativeRight != 0.0) { double maxValueDeltaLeft = derivativeLeft * locationDelta; double maxValueDeltaRight = derivativeRight * locationDelta; double minValue = Math.min(minLeft, minRight); double maxValue = Math.max(maxLeft, maxRight); double minDeltaLeft = maxValueDeltaLeft - maxRight + minLeft; double maxDeltaLeft = maxValueDeltaLeft - minRight + maxLeft; double minDeltaRight = -maxValueDeltaRight + minRight - maxLeft; double maxDeltaRight = -maxValueDeltaRight + maxRight - minLeft; double minDelta = Math.min(minDeltaLeft, minDeltaRight); double maxDelta = Math.max(maxDeltaLeft, maxDeltaRight); splineMin = Math.min(splineMin, minValue + 0.25 * minDelta); splineMax = Math.max(splineMax, maxValue + 0.25 * maxDelta); } } CachedMinMax cachedMinMax = new CachedMinMax(splineMin, splineMax); cache.put(this, cachedMinMax); return cachedMinMax; } @Override public double min() { return minMax().min(); } @Override public double max() { return minMax().max(); } public record Point(double location, CubicSpline value, double derivative) { } private static double linearExtend(double location, Point point, double value) { double derivative = point.derivative(); return derivative == 0.0 ? value : value + derivative * (location - point.location()); } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/math/NumberFunction.java ================================================ package net.minestom.vanilla.datapack.worldgen.math; public interface NumberFunction { double compute(C c); } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/math/SplineInterpolator.java ================================================ package net.minestom.vanilla.datapack.worldgen.math; import it.unimi.dsi.fastutil.doubles.DoubleList; /** * Performs spline interpolation given a set of control points. */ public class SplineInterpolator { private final DoubleList mX; private final DoubleList mY; private final double[] mM; private SplineInterpolator(DoubleList x, DoubleList y, double[] m) { mX = x; mY = y; mM = m; } /** * Creates a monotone cubic spline from a given set of control points. *

* The spline is guaranteed to pass through each control point exactly. Moreover, assuming the control points are * monotonic (Y is non-decreasing or non-increasing) then the interpolated values will also be monotonic. *

* This function uses the Fritsch-Carlson method for computing the spline parameters. * see here * * @param x The X component of the control points, strictly increasing. * @param y The Y component of the control points * @return A monotone cubic spline interpolator. * @throws IllegalArgumentException if the X or Y arrays are null, have different lengths or have fewer than 2 values. */ public static SplineInterpolator createMonotoneCubicSpline(DoubleList x, DoubleList y) { if (x == null || y == null || x.size() != y.size() || x.size() < 2) { throw new IllegalArgumentException("There must be at least two control " + "points and the arrays must be of equal length."); } final int n = x.size(); double[] d = new double[n - 1]; // could optimize this out double[] m = new double[n]; // Compute slopes of secant lines between successive points. for (int i = 0; i < n - 1; i++) { double h = x.getDouble(i + 1) - x.getDouble(i); if (h <= 0f) { throw new IllegalArgumentException("The control points must all " + "have strictly increasing X values."); } d[i] = (y.getDouble(i + 1) - y.getDouble(i)) / h; } // Initialize the tangents as the average of the secants. m[0] = d[0]; for (int i = 1; i < n - 1; i++) { m[i] = (d[i - 1] + d[i]) * 0.5f; } m[n - 1] = d[n - 2]; // Update the tangents to preserve monotonicity. for (int i = 0; i < n - 1; i++) { if (d[i] == 0f) { // successive Y values are equal m[i] = 0f; m[i + 1] = 0f; } else { double a = m[i] / d[i]; double b = m[i + 1] / d[i]; double h = Math.hypot(a, b); if (h > 3f) { double t = 3f / h; m[i] = t * a * d[i]; m[i + 1] = t * b * d[i]; } } } return new SplineInterpolator(x, y, m); } /** * Interpolates the value of Y = f(X) for given X. Clamps X to the domain of the spline. * * @param x The X value. * @return The interpolated Y = f(X) value. */ public double interpolate(double x) { // Handle the boundary cases. final int n = mX.size(); if (Double.isNaN(x)) { return x; } if (x <= mX.getDouble(0)) { return mY.getDouble(0); } if (x >= mX.getDouble(n - 1)) { return mY.getDouble(n - 1); } // Find the index 'i' of the last point with smaller X. // We know this will be within the spline due to the boundary tests. int i = 0; while (x >= mX.getDouble(i + 1)) { i += 1; if (x == mX.getDouble(i)) { return mY.getDouble(i); } } // Perform cubic Hermite spline interpolation. double h = mX.getDouble(i + 1) - mX.getDouble(i); double t = (x - mX.getDouble(i)) / h; return (mY.getDouble(i) * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t) + (mY.getDouble(i + 1) * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t; } // For debugging. @Override public String toString() { StringBuilder str = new StringBuilder(); final int n = mX.size(); str.append("["); for (int i = 0; i < n; i++) { if (i != 0) { str.append(", "); } str.append("(").append(mX.getDouble(i)); str.append(", ").append(mY.getDouble(i)); str.append(": ").append(mM[i]).append(")"); } str.append("]"); return str.toString(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/BlendedNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.util.Util; import org.jetbrains.annotations.Nullable; public class BlendedNoise implements Noise { public final PerlinNoise minLimitNoise; public final PerlinNoise maxLimitNoise; public final PerlinNoise mainNoise; private final double xzMultiplier; private final double yMultiplier; public final double maxValue; // constructor fields private final double xzFactor; private final double yFactor; private final double smearScaleMultiplier; public BlendedNoise(WorldgenRandom random, double xzScale, double yScale, double xzFactor, double yFactor, double smearScaleMultiplier) { this.xzFactor = xzFactor; this.yFactor = yFactor; this.smearScaleMultiplier = smearScaleMultiplier; this.minLimitNoise = new PerlinNoise(random, -15, new double[]{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); this.maxLimitNoise = new PerlinNoise(random, -15, new double[]{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); this.mainNoise = new PerlinNoise(random, -7, new double[]{1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0}); this.xzMultiplier = 684.412 * xzScale; this.yMultiplier = 684.412 * yScale; this.maxValue = this.minLimitNoise.edgeValue(yScale + 2); //TODO } @Override public double sample(double x, double y, double z) { double scaledX = x * this.xzMultiplier; double scaledY = y * this.yMultiplier; double scaledZ = z * this.xzMultiplier; double factoredX = scaledX / this.xzFactor; double factoredY = scaledY / this.yFactor; double factoredZ = scaledZ / this.xzFactor; double smear = this.yMultiplier * this.smearScaleMultiplier; double factoredSmear = smear / this.yFactor; @Nullable ImprovedNoise noise; double value = 0; double factor = 1; for (int i = 0; i < 8; i += 1) { noise = this.mainNoise.getOctaveNoise(i); if (noise != null) { double xx = PerlinNoise.wrap(factoredX * factor); double yy = PerlinNoise.wrap(factoredY * factor); double zz = PerlinNoise.wrap(factoredZ * factor); value += noise.sample(xx, yy, zz, factoredSmear * factor, factoredY * factor) / factor; } factor /= 2; } value = (value / 10 + 1) / 2; factor = 1; double min = 0; double max = 0; for (int i = 0; i < 16; i += 1) { double xx = PerlinNoise.wrap(scaledX * factor); double yy = PerlinNoise.wrap(scaledY * factor); double zz = PerlinNoise.wrap(scaledZ * factor); double smearsmear = smear * factor; if (value < 1 && (noise = this.minLimitNoise.getOctaveNoise(i)) != null) { min += noise.sample(xx, yy, zz, smearsmear, scaledY * factor) / factor; } if (value > 0 && (noise = this.maxLimitNoise.getOctaveNoise(i)) != null) { max += noise.sample(xx, yy, zz, smearsmear, scaledY * factor) / factor; } factor /= 2; } return Util.clampedLerp(min / 512, max / 512, value) / 128; } @Override public double minValue() { return 0; } @Override public double maxValue() { return 1; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/ImprovedNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.util.Util; public class ImprovedNoise implements Noise { public final int[] p; public final double xo; public final double yo; public final double zo; public ImprovedNoise(WorldgenRandom random) { this.xo = random.nextDouble() * 256; this.yo = random.nextDouble() * 256; this.zo = random.nextDouble() * 256; this.p = new int[256]; for (int i = 0; i < 256; i += 1) { this.p[i] = i > 127 ? i - 256 : i; } for (int i = 0; i < 256; i += 1) { int j = random.nextInt(256 - i); int b = this.p[i]; this.p[i] = this.p[i + j]; this.p[i + j] = b; } } public double sample(double x, double y, double z) { return this.sample(x, y, z, 0, 0); } @Override public double minValue() { return -1; } @Override public double maxValue() { return 1; } public double sample(double x, double y, double z, double yScale, double yLimit) { double x2 = x + this.xo; double y2 = y + this.yo; double z2 = z + this.zo; int x3 = (int) Math.floor(x2); int y3 = (int) Math.floor(y2); int z3 = (int) Math.floor(z2); double x4 = x2 - x3; double y4 = y2 - y3; double z4 = z2 - z3; double y6 = 0; if (yScale != 0) { double t = yLimit >= 0 && yLimit < y4 ? yLimit : y4; y6 = Math.floor(t / yScale + 1e-7) * yScale; } return this.sampleAndLerp(x3, y3, z3, x4, y4 - y6, z4, y4); } private double sampleAndLerp(int a, int b, int c, double d, double e, double f, double g) { int h = this.P(a); int i = this.P(a + 1); int j = this.P(h + b); int k = this.P(h + b + 1); int l = this.P(i + b); int m = this.P(i + b + 1); // import { lerp3, smoothstep } from '../Util.js' double n = gradDot(this.P(j + c), d, e, f); double o = gradDot(this.P(l + c), d - 1.0, e, f); double p = gradDot(this.P(k + c), d, e - 1.0, f); double q = gradDot(this.P(m + c), d - 1.0, e - 1.0, f); double r = gradDot(this.P(j + c + 1), d, e, f - 1.0); double s = gradDot(this.P(l + c + 1), d - 1.0, e, f - 1.0); double t = gradDot(this.P(k + c + 1), d, e - 1.0, f - 1.0); double u = gradDot(this.P(m + c + 1), d - 1.0, e - 1.0, f - 1.0); double v = Util.smoothstep(d); double w = Util.smoothstep(g); double x = Util.smoothstep(f); return Util.lerp3(v, w, x, n, o, p, q, r, s, t, u); } public static double gradDot(int a, double b, double c, double d) { var grad = SimplexNoise.GRADIENT[a & 15]; return grad[0] * b + grad[1] * c + grad[2] * d; } private int P(int i) { return this.p[i & 0xFF] & 0xFF; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/LazyLoadedNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.DatapackUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; class LazyLoadedNoise implements Noise { private @Nullable Noise noise = null; public LazyLoadedNoise(String id, DatapackLoader.LoadingContext context) { context.whenFinished(finish -> noise = DatapackUtils.findNoise(finish.datapack(), id).orElseThrow()); } private @NotNull Noise noise() { if (noise == null) { throw new IllegalStateException("Noise not loaded yet"); } return noise; } @Override public double sample(double x, double y, double z) { return noise().sample(x, y, z); } @Override public double minValue() { return noise().minValue(); } @Override public double maxValue() { return noise().maxValue(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/Noise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import com.squareup.moshi.JsonReader; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.json.JsonUtils; import java.io.IOException; public interface Noise { Noise ZERO = new NoiseZero(); double sample(double x, double y, double z); double minValue(); double maxValue(); static Noise fromJson(JsonReader reader) throws IOException { return JsonUtils.typeMap(reader, token -> switch (token) { case STRING -> json -> { // string means use a json-defined noise. This will need to be lazily loaded. String id = json.nextString(); return new LazyLoadedNoise(id, DatapackLoader.loading()); }; case BEGIN_OBJECT -> json -> { NormalNoise.Config params = DatapackLoader.moshi(NormalNoise.Config.class).apply(json); if (DatapackLoader.loading().isStatic()) { // if we're static, to match vanilla we return zero. return Noise.ZERO; } return new NormalNoise(DatapackLoader.loading().random(), params); }; default -> null; }); } } record NoiseZero() implements Noise { @Override public double sample(double x, double y, double z) { return 0; } @Override public double minValue() { return 0; } @Override public double maxValue() { return 0; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/NormalNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import it.unimi.dsi.fastutil.doubles.DoubleList; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; public class NormalNoise implements Noise { public record Config(double firstOctave, DoubleList amplitudes) { } private static final double INPUT_FACTOR = 1.0181268882175227; public final double valueFactor; public final PerlinNoise first; public final PerlinNoise second; public final double maxValue; public NormalNoise(WorldgenRandom random, Config config) { double firstOctave = config.firstOctave(); DoubleList amplitudes = config.amplitudes(); this.first = new PerlinNoise(random, firstOctave, amplitudes); this.second = new PerlinNoise(random, firstOctave, amplitudes); double min = Double.MAX_VALUE; double max = Double.MIN_VALUE; for (int i = 0; i < amplitudes.size(); i += 1) { if (amplitudes.getDouble(i) != 0) { min = Math.min(min, i); max = Math.max(max, i); } } double expectedDeviation = 0.1 * (1 + 1 / (max - min + 1)); this.valueFactor = (1.0 / 6.0) / expectedDeviation; this.maxValue = (this.first.maxValue + this.second.maxValue) * this.valueFactor; } @Override public double sample(double x, double y, double z) { double x2 = x * NormalNoise.INPUT_FACTOR; double y2 = y * NormalNoise.INPUT_FACTOR; double z2 = z * NormalNoise.INPUT_FACTOR; return (this.first.sample(x, y, z) + this.second.sample(x2, y2, z2)) * this.valueFactor; } @Override public double minValue() { return 0; } @Override public double maxValue() { return this.maxValue; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/PerlinNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import it.unimi.dsi.fastutil.doubles.DoubleList; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.random.XoroshiroRandom; public class PerlinNoise implements Noise { public final ImprovedNoise[] noiseLevels; public final double[] amplitudes; public final double lowestFreqInputFactor; public final double lowestFreqValueFactor; public final double maxValue; public PerlinNoise(WorldgenRandom random, double firstOctave, double[] amplitudes) { this(random, firstOctave, DoubleList.of(amplitudes)); } public PerlinNoise(WorldgenRandom random, double firstOctave, DoubleList amplitudes) { this.amplitudes = amplitudes.toDoubleArray(); this.noiseLevels = new ImprovedNoise[amplitudes.size()]; if (random instanceof XoroshiroRandom) { WorldgenRandom.Positional forkedRandom = random.forkPositional(); for (int i = 0; i < amplitudes.size(); i++) { if (amplitudes.getDouble(i) != 0.0) { double octave = firstOctave + i; this.noiseLevels[i] = new ImprovedNoise(forkedRandom.fromHashOf("octave_" + octave)); } } } else { if (1 - firstOctave < amplitudes.size()) { throw new RuntimeException("Positive octaves are not allowed when using LegacyRandom"); } for (int i = (int) -firstOctave; i >= 0; i -= 1) { if (i < amplitudes.size() && amplitudes.getDouble(i) != 0) { this.noiseLevels[i] = new ImprovedNoise(random); } else { random.consumeInt(262); } } } this.lowestFreqInputFactor = Math.pow(2, firstOctave); this.lowestFreqValueFactor = Math.pow(2, (amplitudes.size() - 1)) / (Math.pow(2, amplitudes.size()) - 1); this.maxValue = this.edgeValue(2); } public double sample(double x, double y, double z) { return sample(x, y, z, 0, 0, false); } @Override public double minValue() { return 0; } @Override public double maxValue() { return this.maxValue; } public double sample(double x, double y, double z, double yScale, double yLimit, boolean fixY) { var value = 0.0; double inputF = this.lowestFreqInputFactor; double valueF = this.lowestFreqValueFactor; for (var i = 0; i < this.noiseLevels.length; i += 1) { ImprovedNoise noise = this.noiseLevels[i]; if (noise != null) { value += this.amplitudes[i] * valueF * noise.sample( PerlinNoise.wrap(x * inputF), fixY ? -noise.yo : PerlinNoise.wrap(y * inputF), PerlinNoise.wrap(z * inputF), yScale * inputF, yLimit * inputF); } inputF *= 2; valueF /= 2; } return value; } public ImprovedNoise getOctaveNoise(int i) { return this.noiseLevels[this.noiseLevels.length - 1 - i]; } public double edgeValue(double x) { var value = 0; var valueF = this.lowestFreqValueFactor; for (int i = 0; i < this.noiseLevels.length; i += 1) { if (this.noiseLevels[i] != null) { value += this.amplitudes[i] * x * valueF; } valueF /= 2; } return value; } public static double wrap(double value) { return value - Math.floor(value / 3.3554432E7 + 0.5) * 3.3554432E7; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/noise/SimplexNoise.java ================================================ package net.minestom.vanilla.datapack.worldgen.noise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; public class SimplexNoise implements Noise { static final int[][] GRADIENT = new int[][]{{1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, {0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1}, {1, 1, 0}, {0, -1, 1}, {-1, 1, 0}, {0, -1, -1}}; private static final double F2 = 0.5 * (Math.sqrt(3.0) - 1.0); private static final double G2 = (3.0 - Math.sqrt(3.0)) / 6.0; public final int[] p = new int[512]; public final double xo; public final double yo; public final double zo; public SimplexNoise(WorldgenRandom random) { this.xo = random.nextDouble() * 256.0; this.yo = random.nextDouble() * 256.0; this.zo = random.nextDouble() * 256.0; { int i = 0; while (i < 256) { this.p[i] = i++; } } for (int i = 0; i < 256; ++i) { int j = random.nextInt(256 - i); int b = this.p[i]; this.p[i] = this.p[i + j]; this.p[i + j] = b; } } public double sample2D(double x, double z) { double offset = (x + z) * F2; int offsetA = floor(x + offset); int offsetB = floor(z + offset); double diff = (double) (offsetA + offsetB) * G2; double diffA = (double) offsetA - diff; double diffB = (double) offsetB - diff; double adjustedA = x - diffA; double adjustedB = z - diffB; byte aIsLarger; byte bIsLarger; if (adjustedA > adjustedB) { aIsLarger = 1; bIsLarger = 0; } else { aIsLarger = 0; bIsLarger = 1; } double a1 = adjustedA - (double) aIsLarger + G2; double b1 = adjustedB - (double) bIsLarger + G2; double a2 = adjustedA - 1.0 + 2.0 * G2; double b2 = adjustedB - 1.0 + 2.0 * G2; int a3 = offsetA & 255; int b3 = offsetB & 255; int x1 = this.get(a3 + this.get(b3)) % 12; int y1 = this.get(a3 + aIsLarger + this.get(b3 + bIsLarger)) % 12; int z1 = this.get(a3 + 1 + this.get(b3 + 1)) % 12; double x2 = this.getCornerNoise3D(x1, adjustedA, adjustedB, 0.0, 0.5); double y2 = this.getCornerNoise3D(y1, a1, b1, 0.0, 0.5); double z2 = this.getCornerNoise3D(z1, a2, b2, 0.0, 0.5); return 70.0 * (x2 + y2 + z2); } private static int floor(double x) { int intX = (int) x; return x < (double) intX ? intX - 1 : intX; } public double sample(double x, double y, double z) { double offset = (x + y + z) * 0.3333333333333333; int x1 = floor(x + offset); int y1 = floor(y + offset); int z1 = floor(z + offset); double diff = (double) (x1 + y1 + z1) * 0.16666666666666666; double x2 = (double) x1 - diff; double y2 = (double) y1 - diff; double z2 = (double) z1 - diff; double x3 = x - x2; double y3 = y - y2; double z3 = z - z2; byte x4; byte y4; byte z4; byte x5; byte y5; byte z5; if (x3 >= y3) { if (y3 >= z3) { x4 = 1; y4 = 0; z4 = 0; x5 = 1; y5 = 1; z5 = 0; } else if (x3 >= z3) { x4 = 1; y4 = 0; z4 = 0; x5 = 1; y5 = 0; z5 = 1; } else { x4 = 0; y4 = 0; z4 = 1; x5 = 1; y5 = 0; z5 = 1; } } else if (y3 < z3) { x4 = 0; y4 = 0; z4 = 1; x5 = 0; y5 = 1; z5 = 1; } else if (x3 < z3) { x4 = 0; y4 = 1; z4 = 0; x5 = 0; y5 = 1; z5 = 1; } else { x4 = 0; y4 = 1; z4 = 0; x5 = 1; y5 = 1; z5 = 0; } double x6 = x3 - (double) x4 + 0.16666666666666666; double y6 = y3 - (double) y4 + 0.16666666666666666; double z6 = z3 - (double) z4 + 0.16666666666666666; double x7 = x3 - (double) x5 + 0.3333333333333333; double y7 = y3 - (double) y5 + 0.3333333333333333; double z7 = z3 - (double) z5 + 0.3333333333333333; double x8 = x3 - 1.0 + 0.5; double y8 = y3 - 1.0 + 0.5; double z8 = z3 - 1.0 + 0.5; int x9 = x1 & 255; int y9 = y1 & 255; int z9 = z1 & 255; int a = this.get(x9 + this.get(y9 + this.get(z9))) % 12; int b = this.get(x9 + x4 + this.get(y9 + y4 + this.get(z9 + z4))) % 12; int c = this.get(x9 + x5 + this.get(y9 + y5 + this.get(z9 + z5))) % 12; int d = this.get(x9 + 1 + this.get(y9 + 1 + this.get(z9 + 1))) % 12; double e = this.getCornerNoise3D(a, x3, y3, z3, 0.6); double f = this.getCornerNoise3D(b, x6, y6, z6, 0.6); double g = this.getCornerNoise3D(c, x7, y7, z7, 0.6); double h = this.getCornerNoise3D(d, x8, y8, z8, 0.6); return 32.0 * (e + f + g + h); } @Override public double minValue() { return 0; } @Override public double maxValue() { return 1; } private int get(int i) { return this.p[i & 255]; } private double getCornerNoise3D(int i, double a, double b, double c, double d) { double e = d - a * a - b * b - c * c; double f; if (e < 0.0) { f = 0.0; } else { e *= e; f = e * e * dot(GRADIENT[i], a, b, c); } return f; } protected static double dot(int[] grad, double a, double b, double c) { return (double) grad[0] * a + (double) grad[1] * b + (double) grad[2] * c; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/random/LegacyRandom.java ================================================ package net.minestom.vanilla.datapack.worldgen.random; public class LegacyRandom implements WorldgenRandom { private long seed; public LegacyRandom(long seed) { this.seed = (seed ^ 25214903917L) & 281474976710655L; } @Override public long nextLong() { int high = this.next(32); int low = this.next(32); return ((long) high << 32) + (long) low; } @Override public int nextInt() { return next(32); } @Override public int nextInt(int bound) { if (bound <= 0) { throw new IllegalArgumentException("Bound must be positive"); } if ((bound & bound - 1) == 0) { return (int) ((long) bound * (long) this.next(31) >> 31); } int prev; int out; prev = this.next(31); out = prev % bound; while (prev - out + (bound - 1) < 0) { prev = this.next(31); out = prev % bound; } return out; } @Override public double nextDouble() { int high = this.next(26); int low = this.next(27); long compose = ((long) high << 27) + (long) low; return (double) compose * 1.1102230246251565E-16; } @Override public WorldgenRandom fork() { return new LegacyRandom(this.nextLong()); } public WorldgenRandom.Positional forkPositional() { return new LegacyPositionalRandom(this.nextLong()); } public int next(int max) { long nextSeed = this.seed * 25214903917L + 11L & 281474976710655L; this.seed = nextSeed; return (int) (nextSeed >> 48 - max); } private record LegacyPositionalRandom(long seed) implements WorldgenRandom.Positional { @Override public WorldgenRandom fromHashOf(String name) { return fromSeed(name.hashCode()); } @Override public WorldgenRandom fromSeed(long seed) { return new LegacyRandom(seed ^ this.seed); } @Override public long[] seedKey() { return new long[0]; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/random/MarsagliaPolarGaussian.java ================================================ package net.minestom.vanilla.datapack.worldgen.random; import net.minestom.vanilla.datapack.worldgen.util.Util; class MarsagliaPolarGaussian { public final WorldgenRandom random; private double nextGaussian; private boolean hasNextGaussian; public MarsagliaPolarGaussian(WorldgenRandom random) { this.random = random; } public void reset() { this.hasNextGaussian = false; } public double nextGaussian() { if (this.hasNextGaussian) { this.hasNextGaussian = false; return this.nextGaussian; } else { double a; double b; double c; do { do { a = 2.0 * random.nextDouble() - 1.0; b = 2.0 * random.nextDouble() - 1.0; c = Util.square(a) + Util.square(b); } while(c >= 1.0); } while(c == 0.0); double $$3 = Math.sqrt(-2.0 * Math.log(c) / c); this.nextGaussian = b * $$3; this.hasNextGaussian = true; return a * $$3; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/random/WorldgenRandom.java ================================================ package net.minestom.vanilla.datapack.worldgen.random; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.util.random.RandomGenerator; public interface WorldgenRandom extends RandomGenerator { static WorldgenRandom xoroshiro(long seed) { return new XoroshiroRandom(seed); } static WorldgenRandom legacy(long i) { return new LegacyRandom(i); } long nextLong(); default void consumeInt(int i) { for (int j = 0; j < i; j++) { nextInt(); } } default void consumeLong(int i) { for (int j = 0; j < i; j++) { nextLong(); } } WorldgenRandom fork(); Positional forkPositional(); interface Positional { default WorldgenRandom at(int x, int y, int z) { return fromSeed(Util.getSeed(x, y, z)); } WorldgenRandom fromHashOf(String name); WorldgenRandom fromSeed(long seed); long[] seedKey(); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/random/XoroshiroPositionalRandom.java ================================================ package net.minestom.vanilla.datapack.worldgen.random; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; record XoroshiroPositionalRandom(long seedLow, long seedHigh) implements WorldgenRandom.Positional { @Override public WorldgenRandom fromHashOf(String name) { try { var messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(name.getBytes()); byte[] hash = messageDigest.digest(); long lo = Util.longfromBytes(hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]); long hi = Util.longfromBytes(hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15]); return new XoroshiroRandom(lo ^ this.seedLow, hi ^ this.seedHigh); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Override public WorldgenRandom fromSeed(long seed) { return new XoroshiroRandom(seed ^ seedLow, seedHigh); } @Override public long[] seedKey() { return new long[]{seedLow, seedHigh}; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/random/XoroshiroRandom.java ================================================ package net.minestom.vanilla.datapack.worldgen.random; import net.minestom.vanilla.datapack.worldgen.util.Util; public class XoroshiroRandom implements WorldgenRandom { private final Xoroshiro128PlusPlus xpp; public XoroshiroRandom(long seed) { this(Util.extract128Seed(seed).mixed()); } public XoroshiroRandom(Util.Seed seed) { this(seed.low(), seed.high()); } public XoroshiroRandom(long seedLow, long seedHigh) { this.xpp = new Xoroshiro128PlusPlus(seedLow, seedHigh); } @Override public long nextLong() { return this.xpp.nextLong(); } @Override public int nextInt() { return (int) this.xpp.nextLong(); } @Override public int nextInt(int bound) { if (bound <= 0) { throw new IllegalArgumentException("Bound must be positive"); } long nextUInt = Integer.toUnsignedLong(this.nextInt()); long random = nextUInt * (long) bound; long limit = random & 4294967295L; if (limit < (long) bound) { for (int remainder = Integer.remainderUnsigned(~bound + 1, bound); limit < (long) remainder; limit = random & 0xffffffffL) { nextUInt = Integer.toUnsignedLong(this.nextInt()); random = nextUInt * (long)bound; } } return (int) (random >> 32); } @Override public float nextFloat() { return (float) this.nextBits(24) * 0x1.0p-24f; } @Override public double nextDouble() { return (double) this.nextBits(53) * 0x1.0p-53; } private long nextBits(int bitCount) { return this.xpp.nextLong() >>> 64 - bitCount; } @Override public WorldgenRandom fork() { return new XoroshiroRandom(xpp.nextLong(), xpp.nextLong()); } @Override public Positional forkPositional() { return new XoroshiroPositionalRandom(xpp.nextLong(), xpp.nextLong()); } private static class Xoroshiro128PlusPlus { private long seedLow; private long seedHigh; public Xoroshiro128PlusPlus(long seedLow, long seedHigh) { this.seedLow = seedLow; this.seedHigh = seedHigh; if ((this.seedLow | this.seedHigh) == 0L) { this.seedLow = -7046029254386353131L; this.seedHigh = 7640891576956012809L; } } public long nextLong() { long low = this.seedLow; long high = this.seedHigh; long $$2 = Long.rotateLeft(low + high, 17) + low; high ^= low; this.seedLow = Long.rotateLeft(low, 49) ^ high ^ high << 21; this.seedHigh = Long.rotateLeft(high, 28); return $$2; } } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/storage/DoubleStorage.java ================================================ package net.minestom.vanilla.datapack.worldgen.storage; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import java.util.function.Supplier; public interface DoubleStorage { double obtain(int x, int y, int z); static DoubleStorage from(DensityFunction densityFunction) { return (x, y, z) -> densityFunction.compute(DensityFunction.context(x, y, z)); } /** * A storage that caches an exact, unique value for each 3d coordinate once. * @return a new storage that caches the original */ default DoubleStorage cache() { return new DoubleStorageCache(this); } /** * A storage that caches an exact, unique value for the 2d coordinate (x, z) once. * @return a new storage that caches the original */ default DoubleStorage cache2d() { return new DoubleStorageCache2d(this); } static DoubleStorage threadLocal(Supplier supplier) { return new DoubleStorageThreadLocalImpl(supplier); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/storage/DoubleStorageCache.java ================================================ package net.minestom.vanilla.datapack.worldgen.storage; import it.unimi.dsi.fastutil.longs.Long2DoubleMap; import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; class DoubleStorageCache implements DoubleStorage { private final DoubleStorage original; // The storage private final Long2DoubleMap storage = new Long2DoubleOpenHashMap(); public DoubleStorageCache(DoubleStorage original) { this.original = original; } @Override public double obtain(int x, int y, int z) { long index = getIndex(x, y, z); return storage.computeIfAbsent(index, i -> original.obtain(x, y, z)); } private long getIndex(int x, int y, int z) { // 64 bits, separated into 3x 21 bits // 21 bits for x, 21 bits for y, 21 bits for z long index = 0; index |= (x & 0x1FFFFF); index |= ((long) (y & 0x1FFFFF) << 21); index |= ((long) (z & 0x1FFFFF) << 42); return index; } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/storage/DoubleStorageCache2d.java ================================================ package net.minestom.vanilla.datapack.worldgen.storage; import it.unimi.dsi.fastutil.longs.Long2DoubleMap; import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import net.minestom.server.coordinate.CoordConversion; class DoubleStorageCache2d implements DoubleStorage { private final DoubleStorage original; private final Long2DoubleMap storage = new Long2DoubleOpenHashMap(); public DoubleStorageCache2d(DoubleStorage original) { this.original = original; } @Override public double obtain(int x, int y, int z) { return storage.computeIfAbsent(getIndex(x, z), i -> original.obtain(x, y, z)); } private long getIndex(int x, int z) { return CoordConversion.chunkIndex(x, z); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/storage/DoubleStorageThreadLocalImpl.java ================================================ package net.minestom.vanilla.datapack.worldgen.storage; import java.util.function.Supplier; class DoubleStorageThreadLocalImpl implements DoubleStorage { private final ThreadLocal threadLocal; public DoubleStorageThreadLocalImpl(Supplier supplier) { this.threadLocal = ThreadLocal.withInitial(supplier); } @Override public double obtain(int x, int y, int z) { return threadLocal.get().obtain(x, y, z); } } ================================================ FILE: datapack-loading/src/main/java/net/minestom/vanilla/datapack/worldgen/util/Util.java ================================================ package net.minestom.vanilla.datapack.worldgen.util; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Chunk; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.function.*; import java.util.stream.StreamSupport; public class Util { public static double square(double x) { return x * x; } public static double cube(double x) { return x * x * x; } public static double clamp(double x, double min, double max) { return x < min ? min : Math.min(x, max); } public static double lerp(double a, double b, double c) { return b + a * (c - b); } public static double lerp2(double a, double b, double c, double d, double e, double f) { return lerp(b, lerp(a, c, d), lerp(a, e, f)); } public static double lerp3(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j, double k) { return lerp(c, lerp2(a, b, d, e, f, g), lerp2(a, b, h, i, j, k)); } public static double lazyLerp(double a, DoubleSupplier b, DoubleSupplier c) { if (a == 1) return c.getAsDouble(); if (a == 0) return b.getAsDouble(); double b_value = b.getAsDouble(); return b_value + a * (c.getAsDouble() - b_value); } public static double lazyLerp2(double a, double b, DoubleSupplier c, DoubleSupplier d, DoubleSupplier e, DoubleSupplier f) { return lazyLerp(b, () -> lazyLerp(a, c, d), () -> lazyLerp(a, e, f)); } public static double lazyLerp3(double a, double b, double c, DoubleSupplier d, DoubleSupplier e, DoubleSupplier f, DoubleSupplier g, DoubleSupplier h, DoubleSupplier i, DoubleSupplier j, DoubleSupplier k) { return lazyLerp(c, () -> lazyLerp2(a, b, d, e, f, g), () -> lazyLerp2(a, b, h, i, j, k)); } public static double clampedLerp(double a, double b, double c) { if (c < 0) { return a; } else if (c > 1) { return b; } else { return lerp(c, a, b); } } public static double inverseLerp(double a, double b, double c) { return (a - b) / (c - b); } public static double smoothstep(double x) { return x * x * x * (x * (x * 6.0 - 15.0) + 10.0); } public static double map(double a, double b, double c, double d, double e) { return lerp(inverseLerp(a, b, c), d, e); } public static double clampedMap(double a, double b, double c, double d, double e) { return clampedLerp(d, e, inverseLerp(a, b, c)); } /** * Finds the index of the first value that matches the predicate * @param min inclusive * @param max exclusive */ public static int binarySearch(int min, int max, IntPredicate predicate) { // TODO: Make this an actual binary search // slow version for (int i = min; i < max; i++) { if (predicate.test(i)) { return i; } } return -1; } public static long getSeed(int x, int y, int z) { long seed = (x * 3129871L) ^ (long) z * 116129781L ^ (long) y; seed = seed * seed * 42317861L + seed * 11L; return seed >> 16; } public static long longfromBytes(byte a, byte b, byte c, byte d, byte e, byte f, byte g, byte h) { return (long) (a) << (long) (56) | (long) (b) << (long) (48) | (long) (c) << (long) (40) | (long) (d) << (long) (32) | (long) (e) << (long) (24) | (long) (f) << (long) (16) | (long) (g) << (long) (8) | (long) (h); } public static @NotNull T jsonRequire(JsonObject root, String key, Function mapper) { JsonElement element = root.get(key); if (element == null) { throw new IllegalArgumentException("Missing required key " + key); } return mapper.apply(element); } public static JsonArray jsonArray(JsonElement element) { if (element.isJsonArray()) { return element.getAsJsonArray(); } throw new IllegalArgumentException("Expected array, got " + element); } public static List jsonArray(JsonElement element, Function mapper) { if (element.isJsonArray()) { return StreamSupport.stream(element.getAsJsonArray().spliterator(), false) .map(mapper) .toList(); } throw new IllegalArgumentException("Expected array, got " + element); } public static @NotNull T jsonElse(JsonObject root, String key, T defaultValue, Function mapper) { JsonElement element = root.get(key); if (element == null) { return defaultValue; } return mapper.apply(element); } public static Supplier lazy(Supplier supplier) { return new Supplier<>() { private T value; @Override public T get() { if (value == null) { value = supplier.get(); } return value; } }; } public static IntSupplier lazyInt(IntSupplier supplier) { return new IntSupplier() { private int value; @Override public int getAsInt() { if (value == 0) { value = supplier.getAsInt(); } return value; } }; } public static DoubleSupplier lazyDouble(DoubleSupplier supplier) { return new DoubleSupplier() { private double value; @Override public double getAsDouble() { if (value == 0) { value = supplier.getAsDouble(); } return value; } }; } public static JsonObject jsonObject(Object obj) { if (obj instanceof String str) return jsonObject(new Gson().fromJson(str, JsonElement.class)); if (!(obj instanceof JsonObject object)) throw new IllegalArgumentException("Expected a JsonObject, got " + obj.getClass().getName()); return object; } public static int chunkMinX(Point chunkPos) { int chunkX = chunkPos.chunkX(); return chunkX * Chunk.CHUNK_SIZE_X; } public static int chunkMinZ(Point chunkPos) { int chunkZ = chunkPos.chunkZ(); return chunkZ * Chunk.CHUNK_SIZE_Z; } public static int chunkMaxX(Point chunkPos) { return chunkMinX(chunkPos) + Chunk.CHUNK_SIZE_X; } public static int chunkMaxZ(Point chunkPos) { return chunkMinZ(chunkPos) + Chunk.CHUNK_SIZE_Z; } public record Seed(long low, long high) { public Seed mixed() { return new Seed(staffordMix13(low), staffordMix13(high)); } } public static Seed extract128Seed(long originalSeed) { long low = originalSeed ^ 0x6a09e667f3bcc909L; long high = low - 0x61c8864680b583ebL; return new Seed(low, high); } /* David Stafford's (http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html) * "Mix13" variant of the 64-bit finalizer in Austin Appleby's MurmurHash3 algorithm. */ public static long staffordMix13(long z) { z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; return z ^ (z >>> 31); } } ================================================ FILE: datapack-tests/build.gradle.kts ================================================ plugins { id("org.spongepowered.gradle.vanilla") version "0.2.1-SNAPSHOT" } dependencies { compileOnly(project(":core")) testImplementation(platform("org.junit:junit-bom:5.9.1")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation(project(":core")) testImplementation(project(":datapack-loading")) testImplementation(project(":blocks")) testImplementation(project(":block-update-system")) testImplementation(project(":mojang-data")) } tasks.test { useJUnitPlatform() } minecraft { version("1.21.5") runs { server() } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/loot/LootTableTestData.java ================================================ package net.minestom.vanilla.datapack.loot; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.datapack.loot.context.LootContext; import java.util.HashMap; import java.util.List; import java.util.Map; public class LootTableTestData { public static final Map> EXPECTED_RESULTS; static { Map> expected = new HashMap<>(); // TODO: Go through these and make sure they are correct expected.put("acacia_button", List.of(ItemStack.of(Material.ACACIA_BUTTON, 1))); expected.put("acacia_door", List.of()); expected.put("acacia_fence", List.of(ItemStack.of(Material.ACACIA_FENCE, 1))); expected.put("acacia_fence_gate", List.of(ItemStack.of(Material.ACACIA_FENCE_GATE, 1))); expected.put("acacia_hanging_sign", List.of(ItemStack.of(Material.ACACIA_HANGING_SIGN, 1))); expected.put("acacia_leaves", List.of()); expected.put("acacia_log", List.of(ItemStack.of(Material.ACACIA_LOG, 1))); expected.put("acacia_planks", List.of(ItemStack.of(Material.ACACIA_PLANKS, 1))); expected.put("acacia_pressure_plate", List.of(ItemStack.of(Material.ACACIA_PRESSURE_PLATE, 1))); expected.put("acacia_sapling", List.of(ItemStack.of(Material.ACACIA_SAPLING, 1))); expected.put("acacia_sign", List.of(ItemStack.of(Material.ACACIA_SIGN, 1))); expected.put("acacia_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("acacia_stairs", List.of(ItemStack.of(Material.ACACIA_STAIRS, 1))); expected.put("acacia_trapdoor", List.of(ItemStack.of(Material.ACACIA_TRAPDOOR, 1))); expected.put("acacia_wood", List.of(ItemStack.of(Material.ACACIA_WOOD, 1))); expected.put("activator_rail", List.of(ItemStack.of(Material.ACTIVATOR_RAIL, 1))); expected.put("allium", List.of(ItemStack.of(Material.ALLIUM, 1))); expected.put("amethyst_block", List.of(ItemStack.of(Material.AMETHYST_BLOCK, 1))); expected.put("amethyst_cluster", List.of(ItemStack.of(Material.AIR, 1))); expected.put("ancient_debris", List.of(ItemStack.of(Material.ANCIENT_DEBRIS, 1))); expected.put("andesite", List.of(ItemStack.of(Material.ANDESITE, 1))); expected.put("andesite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("andesite_stairs", List.of(ItemStack.of(Material.ANDESITE_STAIRS, 1))); expected.put("andesite_wall", List.of(ItemStack.of(Material.ANDESITE_WALL, 1))); expected.put("anvil", List.of(ItemStack.of(Material.ANVIL, 1))); expected.put("attached_melon_stem", List.of(ItemStack.of(Material.AIR, 1))); expected.put("attached_pumpkin_stem", List.of(ItemStack.of(Material.AIR, 1))); expected.put("azalea", List.of(ItemStack.of(Material.AZALEA, 1))); expected.put("azalea_leaves", List.of()); expected.put("azure_bluet", List.of(ItemStack.of(Material.AZURE_BLUET, 1))); expected.put("bamboo", List.of(ItemStack.of(Material.BAMBOO, 1))); expected.put("bamboo_block", List.of(ItemStack.of(Material.BAMBOO_BLOCK, 1))); expected.put("bamboo_button", List.of(ItemStack.of(Material.BAMBOO_BUTTON, 1))); expected.put("bamboo_door", List.of()); expected.put("bamboo_fence", List.of(ItemStack.of(Material.BAMBOO_FENCE, 1))); expected.put("bamboo_fence_gate", List.of(ItemStack.of(Material.BAMBOO_FENCE_GATE, 1))); expected.put("bamboo_hanging_sign", List.of(ItemStack.of(Material.BAMBOO_HANGING_SIGN, 1))); expected.put("bamboo_mosaic", List.of(ItemStack.of(Material.BAMBOO_MOSAIC, 1))); expected.put("bamboo_mosaic_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("bamboo_mosaic_stairs", List.of(ItemStack.of(Material.BAMBOO_MOSAIC_STAIRS, 1))); expected.put("bamboo_planks", List.of(ItemStack.of(Material.BAMBOO_PLANKS, 1))); expected.put("bamboo_pressure_plate", List.of(ItemStack.of(Material.BAMBOO_PRESSURE_PLATE, 1))); expected.put("bamboo_sapling", List.of(ItemStack.of(Material.BAMBOO, 1))); expected.put("bamboo_sign", List.of(ItemStack.of(Material.BAMBOO_SIGN, 1))); expected.put("bamboo_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("bamboo_stairs", List.of(ItemStack.of(Material.BAMBOO_STAIRS, 1))); expected.put("bamboo_trapdoor", List.of(ItemStack.of(Material.BAMBOO_TRAPDOOR, 1))); expected.put("barrel", List.of(ItemStack.of(Material.BARREL, 1))); expected.put("basalt", List.of(ItemStack.of(Material.BASALT, 1))); expected.put("beacon", List.of(ItemStack.of(Material.BEACON, 1))); expected.put("bee_nest", List.of()); expected.put("beehive", List.of(ItemStack.of(Material.BEEHIVE, 1))); expected.put("beetroots", List.of(ItemStack.of(Material.AIR, 1))); expected.put("bell", List.of(ItemStack.of(Material.BELL, 1))); expected.put("big_dripleaf", List.of(ItemStack.of(Material.BIG_DRIPLEAF, 1))); expected.put("big_dripleaf_stem", List.of(ItemStack.of(Material.BIG_DRIPLEAF, 1))); expected.put("birch_button", List.of(ItemStack.of(Material.BIRCH_BUTTON, 1))); expected.put("birch_door", List.of()); expected.put("birch_fence", List.of(ItemStack.of(Material.BIRCH_FENCE, 1))); expected.put("birch_fence_gate", List.of(ItemStack.of(Material.BIRCH_FENCE_GATE, 1))); expected.put("birch_hanging_sign", List.of(ItemStack.of(Material.BIRCH_HANGING_SIGN, 1))); expected.put("birch_leaves", List.of()); expected.put("birch_log", List.of(ItemStack.of(Material.BIRCH_LOG, 1))); expected.put("birch_planks", List.of(ItemStack.of(Material.BIRCH_PLANKS, 1))); expected.put("birch_pressure_plate", List.of(ItemStack.of(Material.BIRCH_PRESSURE_PLATE, 1))); expected.put("birch_sapling", List.of(ItemStack.of(Material.BIRCH_SAPLING, 1))); expected.put("birch_sign", List.of(ItemStack.of(Material.BIRCH_SIGN, 1))); expected.put("birch_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("birch_stairs", List.of(ItemStack.of(Material.BIRCH_STAIRS, 1))); expected.put("birch_trapdoor", List.of(ItemStack.of(Material.BIRCH_TRAPDOOR, 1))); expected.put("birch_wood", List.of(ItemStack.of(Material.BIRCH_WOOD, 1))); expected.put("black_banner", List.of(ItemStack.of(Material.BLACK_BANNER, 1))); expected.put("black_bed", List.of()); expected.put("black_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("black_candle_cake", List.of(ItemStack.of(Material.BLACK_CANDLE, 1))); expected.put("black_carpet", List.of(ItemStack.of(Material.BLACK_CARPET, 1))); expected.put("black_concrete", List.of(ItemStack.of(Material.BLACK_CONCRETE, 1))); expected.put("black_concrete_powder", List.of(ItemStack.of(Material.BLACK_CONCRETE_POWDER, 1))); expected.put("black_glazed_terracotta", List.of(ItemStack.of(Material.BLACK_GLAZED_TERRACOTTA, 1))); expected.put("black_shulker_box", List.of(ItemStack.of(Material.BLACK_SHULKER_BOX, 1))); expected.put("black_stained_glass", List.of()); expected.put("black_stained_glass_pane", List.of()); expected.put("black_terracotta", List.of(ItemStack.of(Material.BLACK_TERRACOTTA, 1))); expected.put("black_wool", List.of(ItemStack.of(Material.BLACK_WOOL, 1))); expected.put("blackstone", List.of(ItemStack.of(Material.BLACKSTONE, 1))); expected.put("blackstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("blackstone_stairs", List.of(ItemStack.of(Material.BLACKSTONE_STAIRS, 1))); expected.put("blackstone_wall", List.of(ItemStack.of(Material.BLACKSTONE_WALL, 1))); expected.put("blast_furnace", List.of(ItemStack.of(Material.BLAST_FURNACE, 1))); expected.put("blue_banner", List.of(ItemStack.of(Material.BLUE_BANNER, 1))); expected.put("blue_bed", List.of()); expected.put("blue_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("blue_candle_cake", List.of(ItemStack.of(Material.BLUE_CANDLE, 1))); expected.put("blue_carpet", List.of(ItemStack.of(Material.BLUE_CARPET, 1))); expected.put("blue_concrete", List.of(ItemStack.of(Material.BLUE_CONCRETE, 1))); expected.put("blue_concrete_powder", List.of(ItemStack.of(Material.BLUE_CONCRETE_POWDER, 1))); expected.put("blue_glazed_terracotta", List.of(ItemStack.of(Material.BLUE_GLAZED_TERRACOTTA, 1))); expected.put("blue_ice", List.of()); expected.put("blue_orchid", List.of(ItemStack.of(Material.BLUE_ORCHID, 1))); expected.put("blue_shulker_box", List.of(ItemStack.of(Material.BLUE_SHULKER_BOX, 1))); expected.put("blue_stained_glass", List.of()); expected.put("blue_stained_glass_pane", List.of()); expected.put("blue_terracotta", List.of(ItemStack.of(Material.BLUE_TERRACOTTA, 1))); expected.put("blue_wool", List.of(ItemStack.of(Material.BLUE_WOOL, 1))); expected.put("bone_block", List.of(ItemStack.of(Material.BONE_BLOCK, 1))); expected.put("bookshelf", List.of(ItemStack.of(Material.AIR, 1))); expected.put("brain_coral", List.of()); expected.put("brain_coral_block", List.of(ItemStack.of(Material.DEAD_BRAIN_CORAL_BLOCK, 1))); expected.put("brain_coral_fan", List.of()); expected.put("brewing_stand", List.of(ItemStack.of(Material.BREWING_STAND, 1))); expected.put("brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("brick_stairs", List.of(ItemStack.of(Material.BRICK_STAIRS, 1))); expected.put("brick_wall", List.of(ItemStack.of(Material.BRICK_WALL, 1))); expected.put("bricks", List.of(ItemStack.of(Material.BRICKS, 1))); expected.put("brown_banner", List.of(ItemStack.of(Material.BROWN_BANNER, 1))); expected.put("brown_bed", List.of()); expected.put("brown_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("brown_candle_cake", List.of(ItemStack.of(Material.BROWN_CANDLE, 1))); expected.put("brown_carpet", List.of(ItemStack.of(Material.BROWN_CARPET, 1))); expected.put("brown_concrete", List.of(ItemStack.of(Material.BROWN_CONCRETE, 1))); expected.put("brown_concrete_powder", List.of(ItemStack.of(Material.BROWN_CONCRETE_POWDER, 1))); expected.put("brown_glazed_terracotta", List.of(ItemStack.of(Material.BROWN_GLAZED_TERRACOTTA, 1))); expected.put("brown_mushroom", List.of(ItemStack.of(Material.BROWN_MUSHROOM, 1))); expected.put("brown_mushroom_block", List.of(ItemStack.of(Material.AIR, 1))); expected.put("brown_shulker_box", List.of(ItemStack.of(Material.BROWN_SHULKER_BOX, 1))); expected.put("brown_stained_glass", List.of()); expected.put("brown_stained_glass_pane", List.of()); expected.put("brown_terracotta", List.of(ItemStack.of(Material.BROWN_TERRACOTTA, 1))); expected.put("brown_wool", List.of(ItemStack.of(Material.BROWN_WOOL, 1))); expected.put("bubble_coral", List.of()); expected.put("bubble_coral_block", List.of(ItemStack.of(Material.DEAD_BUBBLE_CORAL_BLOCK, 1))); expected.put("bubble_coral_fan", List.of()); expected.put("budding_amethyst", List.of()); expected.put("cactus", List.of(ItemStack.of(Material.CACTUS, 1))); expected.put("cake", List.of()); expected.put("calcite", List.of(ItemStack.of(Material.CALCITE, 1))); expected.put("calibrated_sculk_sensor", List.of()); expected.put("campfire", List.of(ItemStack.of(Material.CHARCOAL, 2))); expected.put("candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("candle_cake", List.of(ItemStack.of(Material.CANDLE, 1))); expected.put("carrots", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cartography_table", List.of(ItemStack.of(Material.CARTOGRAPHY_TABLE, 1))); expected.put("carved_pumpkin", List.of(ItemStack.of(Material.CARVED_PUMPKIN, 1))); expected.put("cauldron", List.of(ItemStack.of(Material.CAULDRON, 1))); expected.put("cave_vines", List.of()); expected.put("cave_vines_plant", List.of()); expected.put("chain", List.of(ItemStack.of(Material.CHAIN, 1))); expected.put("cherry_button", List.of(ItemStack.of(Material.CHERRY_BUTTON, 1))); expected.put("cherry_door", List.of()); expected.put("cherry_fence", List.of(ItemStack.of(Material.CHERRY_FENCE, 1))); expected.put("cherry_fence_gate", List.of(ItemStack.of(Material.CHERRY_FENCE_GATE, 1))); expected.put("cherry_hanging_sign", List.of(ItemStack.of(Material.CHERRY_HANGING_SIGN, 1))); expected.put("cherry_leaves", List.of()); expected.put("cherry_log", List.of(ItemStack.of(Material.CHERRY_LOG, 1))); expected.put("cherry_planks", List.of(ItemStack.of(Material.CHERRY_PLANKS, 1))); expected.put("cherry_pressure_plate", List.of(ItemStack.of(Material.CHERRY_PRESSURE_PLATE, 1))); expected.put("cherry_sapling", List.of(ItemStack.of(Material.CHERRY_SAPLING, 1))); expected.put("cherry_sign", List.of(ItemStack.of(Material.CHERRY_SIGN, 1))); expected.put("cherry_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cherry_stairs", List.of(ItemStack.of(Material.CHERRY_STAIRS, 1))); expected.put("cherry_trapdoor", List.of(ItemStack.of(Material.CHERRY_TRAPDOOR, 1))); expected.put("cherry_wood", List.of(ItemStack.of(Material.CHERRY_WOOD, 1))); expected.put("chest", List.of(ItemStack.of(Material.CHEST, 1))); expected.put("chipped_anvil", List.of(ItemStack.of(Material.CHIPPED_ANVIL, 1))); expected.put("chiseled_bookshelf", List.of()); expected.put("chiseled_copper", List.of()); expected.put("chiseled_deepslate", List.of(ItemStack.of(Material.CHISELED_DEEPSLATE, 1))); expected.put("chiseled_nether_bricks", List.of(ItemStack.of(Material.CHISELED_NETHER_BRICKS, 1))); expected.put("chiseled_polished_blackstone", List.of(ItemStack.of(Material.CHISELED_POLISHED_BLACKSTONE, 1))); expected.put("chiseled_quartz_block", List.of(ItemStack.of(Material.CHISELED_QUARTZ_BLOCK, 1))); expected.put("chiseled_red_sandstone", List.of(ItemStack.of(Material.CHISELED_RED_SANDSTONE, 1))); expected.put("chiseled_sandstone", List.of(ItemStack.of(Material.CHISELED_SANDSTONE, 1))); expected.put("chiseled_stone_bricks", List.of(ItemStack.of(Material.CHISELED_STONE_BRICKS, 1))); expected.put("chiseled_tuff", List.of()); expected.put("chiseled_tuff_bricks", List.of()); expected.put("chorus_flower", List.of()); expected.put("chorus_plant", List.of(ItemStack.of(Material.AIR, 1))); expected.put("clay", List.of(ItemStack.of(Material.AIR, 1))); expected.put("coal_block", List.of(ItemStack.of(Material.COAL_BLOCK, 1))); expected.put("coal_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("coarse_dirt", List.of(ItemStack.of(Material.COARSE_DIRT, 1))); expected.put("cobbled_deepslate", List.of(ItemStack.of(Material.COBBLED_DEEPSLATE, 1))); expected.put("cobbled_deepslate_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cobbled_deepslate_stairs", List.of(ItemStack.of(Material.COBBLED_DEEPSLATE_STAIRS, 1))); expected.put("cobbled_deepslate_wall", List.of(ItemStack.of(Material.COBBLED_DEEPSLATE_WALL, 1))); expected.put("cobblestone", List.of(ItemStack.of(Material.COBBLESTONE, 1))); expected.put("cobblestone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cobblestone_stairs", List.of(ItemStack.of(Material.COBBLESTONE_STAIRS, 1))); expected.put("cobblestone_wall", List.of(ItemStack.of(Material.COBBLESTONE_WALL, 1))); expected.put("cobweb", List.of(ItemStack.of(Material.STRING, 1))); expected.put("cocoa", List.of(ItemStack.of(Material.AIR, 1))); expected.put("comparator", List.of(ItemStack.of(Material.COMPARATOR, 1))); expected.put("composter", List.of(ItemStack.of(Material.AIR, 1))); expected.put("conduit", List.of(ItemStack.of(Material.CONDUIT, 1))); expected.put("copper_block", List.of(ItemStack.of(Material.COPPER_BLOCK, 1))); expected.put("copper_bulb", List.of()); expected.put("copper_door", List.of()); expected.put("copper_grate", List.of()); expected.put("copper_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("copper_trapdoor", List.of()); expected.put("cornflower", List.of(ItemStack.of(Material.CORNFLOWER, 1))); expected.put("cracked_deepslate_bricks", List.of(ItemStack.of(Material.CRACKED_DEEPSLATE_BRICKS, 1))); expected.put("cracked_deepslate_tiles", List.of(ItemStack.of(Material.CRACKED_DEEPSLATE_TILES, 1))); expected.put("cracked_nether_bricks", List.of(ItemStack.of(Material.CRACKED_NETHER_BRICKS, 1))); expected.put("cracked_polished_blackstone_bricks", List.of(ItemStack.of(Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, 1))); expected.put("cracked_stone_bricks", List.of(ItemStack.of(Material.CRACKED_STONE_BRICKS, 1))); expected.put("crafter", List.of()); expected.put("crafting_table", List.of(ItemStack.of(Material.CRAFTING_TABLE, 1))); expected.put("creeper_head", List.of(ItemStack.of(Material.CREEPER_HEAD, 1))); expected.put("crimson_button", List.of(ItemStack.of(Material.CRIMSON_BUTTON, 1))); expected.put("crimson_door", List.of()); expected.put("crimson_fence", List.of(ItemStack.of(Material.CRIMSON_FENCE, 1))); expected.put("crimson_fence_gate", List.of(ItemStack.of(Material.CRIMSON_FENCE_GATE, 1))); expected.put("crimson_fungus", List.of(ItemStack.of(Material.CRIMSON_FUNGUS, 1))); expected.put("crimson_hanging_sign", List.of(ItemStack.of(Material.CRIMSON_HANGING_SIGN, 1))); expected.put("crimson_hyphae", List.of(ItemStack.of(Material.CRIMSON_HYPHAE, 1))); expected.put("crimson_nylium", List.of(ItemStack.of(Material.NETHERRACK, 1))); expected.put("crimson_planks", List.of(ItemStack.of(Material.CRIMSON_PLANKS, 1))); expected.put("crimson_pressure_plate", List.of(ItemStack.of(Material.CRIMSON_PRESSURE_PLATE, 1))); expected.put("crimson_roots", List.of(ItemStack.of(Material.CRIMSON_ROOTS, 1))); expected.put("crimson_sign", List.of(ItemStack.of(Material.CRIMSON_SIGN, 1))); expected.put("crimson_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("crimson_stairs", List.of(ItemStack.of(Material.CRIMSON_STAIRS, 1))); expected.put("crimson_stem", List.of(ItemStack.of(Material.CRIMSON_STEM, 1))); expected.put("crimson_trapdoor", List.of(ItemStack.of(Material.CRIMSON_TRAPDOOR, 1))); expected.put("crying_obsidian", List.of(ItemStack.of(Material.CRYING_OBSIDIAN, 1))); expected.put("cut_copper", List.of(ItemStack.of(Material.CUT_COPPER, 1))); expected.put("cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cut_copper_stairs", List.of(ItemStack.of(Material.CUT_COPPER_STAIRS, 1))); expected.put("cut_red_sandstone", List.of(ItemStack.of(Material.CUT_RED_SANDSTONE, 1))); expected.put("cut_red_sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cut_sandstone", List.of(ItemStack.of(Material.CUT_SANDSTONE, 1))); expected.put("cut_sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cyan_banner", List.of(ItemStack.of(Material.CYAN_BANNER, 1))); expected.put("cyan_bed", List.of()); expected.put("cyan_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("cyan_candle_cake", List.of(ItemStack.of(Material.CYAN_CANDLE, 1))); expected.put("cyan_carpet", List.of(ItemStack.of(Material.CYAN_CARPET, 1))); expected.put("cyan_concrete", List.of(ItemStack.of(Material.CYAN_CONCRETE, 1))); expected.put("cyan_concrete_powder", List.of(ItemStack.of(Material.CYAN_CONCRETE_POWDER, 1))); expected.put("cyan_glazed_terracotta", List.of(ItemStack.of(Material.CYAN_GLAZED_TERRACOTTA, 1))); expected.put("cyan_shulker_box", List.of(ItemStack.of(Material.CYAN_SHULKER_BOX, 1))); expected.put("cyan_stained_glass", List.of()); expected.put("cyan_stained_glass_pane", List.of()); expected.put("cyan_terracotta", List.of(ItemStack.of(Material.CYAN_TERRACOTTA, 1))); expected.put("cyan_wool", List.of(ItemStack.of(Material.CYAN_WOOL, 1))); expected.put("damaged_anvil", List.of(ItemStack.of(Material.DAMAGED_ANVIL, 1))); expected.put("dandelion", List.of(ItemStack.of(Material.DANDELION, 1))); expected.put("dark_oak_button", List.of(ItemStack.of(Material.DARK_OAK_BUTTON, 1))); expected.put("dark_oak_door", List.of()); expected.put("dark_oak_fence", List.of(ItemStack.of(Material.DARK_OAK_FENCE, 1))); expected.put("dark_oak_fence_gate", List.of(ItemStack.of(Material.DARK_OAK_FENCE_GATE, 1))); expected.put("dark_oak_hanging_sign", List.of(ItemStack.of(Material.DARK_OAK_HANGING_SIGN, 1))); expected.put("dark_oak_leaves", List.of()); expected.put("dark_oak_log", List.of(ItemStack.of(Material.DARK_OAK_LOG, 1))); expected.put("dark_oak_planks", List.of(ItemStack.of(Material.DARK_OAK_PLANKS, 1))); expected.put("dark_oak_pressure_plate", List.of(ItemStack.of(Material.DARK_OAK_PRESSURE_PLATE, 1))); expected.put("dark_oak_sapling", List.of(ItemStack.of(Material.DARK_OAK_SAPLING, 1))); expected.put("dark_oak_sign", List.of(ItemStack.of(Material.DARK_OAK_SIGN, 1))); expected.put("dark_oak_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("dark_oak_stairs", List.of(ItemStack.of(Material.DARK_OAK_STAIRS, 1))); expected.put("dark_oak_trapdoor", List.of(ItemStack.of(Material.DARK_OAK_TRAPDOOR, 1))); expected.put("dark_oak_wood", List.of(ItemStack.of(Material.DARK_OAK_WOOD, 1))); expected.put("dark_prismarine", List.of(ItemStack.of(Material.DARK_PRISMARINE, 1))); expected.put("dark_prismarine_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("dark_prismarine_stairs", List.of(ItemStack.of(Material.DARK_PRISMARINE_STAIRS, 1))); expected.put("daylight_detector", List.of(ItemStack.of(Material.DAYLIGHT_DETECTOR, 1))); expected.put("dead_brain_coral", List.of()); expected.put("dead_brain_coral_block", List.of(ItemStack.of(Material.DEAD_BRAIN_CORAL_BLOCK, 1))); expected.put("dead_brain_coral_fan", List.of()); expected.put("dead_bubble_coral", List.of()); expected.put("dead_bubble_coral_block", List.of(ItemStack.of(Material.DEAD_BUBBLE_CORAL_BLOCK, 1))); expected.put("dead_bubble_coral_fan", List.of()); expected.put("dead_bush", List.of(ItemStack.of(Material.AIR, 1))); expected.put("dead_fire_coral", List.of()); expected.put("dead_fire_coral_block", List.of(ItemStack.of(Material.DEAD_FIRE_CORAL_BLOCK, 1))); expected.put("dead_fire_coral_fan", List.of()); expected.put("dead_horn_coral", List.of()); expected.put("dead_horn_coral_block", List.of(ItemStack.of(Material.DEAD_HORN_CORAL_BLOCK, 1))); expected.put("dead_horn_coral_fan", List.of()); expected.put("dead_tube_coral", List.of()); expected.put("dead_tube_coral_block", List.of(ItemStack.of(Material.DEAD_TUBE_CORAL_BLOCK, 1))); expected.put("dead_tube_coral_fan", List.of()); expected.put("decorated_pot", List.of(ItemStack.of(Material.DECORATED_POT, 1))); expected.put("deepslate", List.of(ItemStack.of(Material.COBBLED_DEEPSLATE, 1))); expected.put("deepslate_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_brick_stairs", List.of(ItemStack.of(Material.DEEPSLATE_BRICK_STAIRS, 1))); expected.put("deepslate_brick_wall", List.of(ItemStack.of(Material.DEEPSLATE_BRICK_WALL, 1))); expected.put("deepslate_bricks", List.of(ItemStack.of(Material.DEEPSLATE_BRICKS, 1))); expected.put("deepslate_coal_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_copper_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_diamond_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_emerald_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_gold_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_iron_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_lapis_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_redstone_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_tile_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("deepslate_tile_stairs", List.of(ItemStack.of(Material.DEEPSLATE_TILE_STAIRS, 1))); expected.put("deepslate_tile_wall", List.of(ItemStack.of(Material.DEEPSLATE_TILE_WALL, 1))); expected.put("deepslate_tiles", List.of(ItemStack.of(Material.DEEPSLATE_TILES, 1))); expected.put("detector_rail", List.of(ItemStack.of(Material.DETECTOR_RAIL, 1))); expected.put("diamond_block", List.of(ItemStack.of(Material.DIAMOND_BLOCK, 1))); expected.put("diamond_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("diorite", List.of(ItemStack.of(Material.DIORITE, 1))); expected.put("diorite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("diorite_stairs", List.of(ItemStack.of(Material.DIORITE_STAIRS, 1))); expected.put("diorite_wall", List.of(ItemStack.of(Material.DIORITE_WALL, 1))); expected.put("dirt", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("dirt_path", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("dispenser", List.of(ItemStack.of(Material.DISPENSER, 1))); expected.put("dragon_egg", List.of(ItemStack.of(Material.DRAGON_EGG, 1))); expected.put("dragon_head", List.of(ItemStack.of(Material.DRAGON_HEAD, 1))); expected.put("dried_kelp_block", List.of(ItemStack.of(Material.DRIED_KELP_BLOCK, 1))); expected.put("dripstone_block", List.of(ItemStack.of(Material.DRIPSTONE_BLOCK, 1))); expected.put("dropper", List.of(ItemStack.of(Material.DROPPER, 1))); expected.put("emerald_block", List.of(ItemStack.of(Material.EMERALD_BLOCK, 1))); expected.put("emerald_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("enchanting_table", List.of(ItemStack.of(Material.ENCHANTING_TABLE, 1))); expected.put("end_rod", List.of(ItemStack.of(Material.END_ROD, 1))); expected.put("end_stone", List.of(ItemStack.of(Material.END_STONE, 1))); expected.put("end_stone_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("end_stone_brick_stairs", List.of(ItemStack.of(Material.END_STONE_BRICK_STAIRS, 1))); expected.put("end_stone_brick_wall", List.of(ItemStack.of(Material.END_STONE_BRICK_WALL, 1))); expected.put("end_stone_bricks", List.of(ItemStack.of(Material.END_STONE_BRICKS, 1))); expected.put("ender_chest", List.of(ItemStack.of(Material.AIR, 1))); expected.put("exposed_chiseled_copper", List.of()); expected.put("exposed_copper", List.of(ItemStack.of(Material.EXPOSED_COPPER, 1))); expected.put("exposed_copper_bulb", List.of()); expected.put("exposed_copper_door", List.of()); expected.put("exposed_copper_grate", List.of()); expected.put("exposed_copper_trapdoor", List.of()); expected.put("exposed_cut_copper", List.of(ItemStack.of(Material.EXPOSED_CUT_COPPER, 1))); expected.put("exposed_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("exposed_cut_copper_stairs", List.of(ItemStack.of(Material.EXPOSED_CUT_COPPER_STAIRS, 1))); expected.put("farmland", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("fern", List.of()); expected.put("fire", List.of()); expected.put("fire_coral", List.of()); expected.put("fire_coral_block", List.of(ItemStack.of(Material.DEAD_FIRE_CORAL_BLOCK, 1))); expected.put("fire_coral_fan", List.of()); expected.put("fletching_table", List.of(ItemStack.of(Material.FLETCHING_TABLE, 1))); expected.put("flower_pot", List.of(ItemStack.of(Material.FLOWER_POT, 1))); expected.put("flowering_azalea", List.of(ItemStack.of(Material.FLOWERING_AZALEA, 1))); expected.put("flowering_azalea_leaves", List.of()); expected.put("frogspawn", List.of()); expected.put("frosted_ice", List.of()); expected.put("furnace", List.of(ItemStack.of(Material.FURNACE, 1))); expected.put("gilded_blackstone", List.of(ItemStack.of(Material.GILDED_BLACKSTONE, 1))); expected.put("glass", List.of()); expected.put("glass_pane", List.of()); expected.put("glow_lichen", List.of()); expected.put("glowstone", List.of(ItemStack.of(Material.AIR, 1))); expected.put("gold_block", List.of(ItemStack.of(Material.GOLD_BLOCK, 1))); expected.put("gold_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("granite", List.of(ItemStack.of(Material.GRANITE, 1))); expected.put("granite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("granite_stairs", List.of(ItemStack.of(Material.GRANITE_STAIRS, 1))); expected.put("granite_wall", List.of(ItemStack.of(Material.GRANITE_WALL, 1))); expected.put("grass_block", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("gravel", List.of(ItemStack.of(Material.GRAVEL, 1))); expected.put("gray_banner", List.of(ItemStack.of(Material.GRAY_BANNER, 1))); expected.put("gray_bed", List.of()); expected.put("gray_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("gray_candle_cake", List.of(ItemStack.of(Material.GRAY_CANDLE, 1))); expected.put("gray_carpet", List.of(ItemStack.of(Material.GRAY_CARPET, 1))); expected.put("gray_concrete", List.of(ItemStack.of(Material.GRAY_CONCRETE, 1))); expected.put("gray_concrete_powder", List.of(ItemStack.of(Material.GRAY_CONCRETE_POWDER, 1))); expected.put("gray_glazed_terracotta", List.of(ItemStack.of(Material.GRAY_GLAZED_TERRACOTTA, 1))); expected.put("gray_shulker_box", List.of(ItemStack.of(Material.GRAY_SHULKER_BOX, 1))); expected.put("gray_stained_glass", List.of()); expected.put("gray_stained_glass_pane", List.of()); expected.put("gray_terracotta", List.of(ItemStack.of(Material.GRAY_TERRACOTTA, 1))); expected.put("gray_wool", List.of(ItemStack.of(Material.GRAY_WOOL, 1))); expected.put("green_banner", List.of(ItemStack.of(Material.GREEN_BANNER, 1))); expected.put("green_bed", List.of()); expected.put("green_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("green_candle_cake", List.of(ItemStack.of(Material.GREEN_CANDLE, 1))); expected.put("green_carpet", List.of(ItemStack.of(Material.GREEN_CARPET, 1))); expected.put("green_concrete", List.of(ItemStack.of(Material.GREEN_CONCRETE, 1))); expected.put("green_concrete_powder", List.of(ItemStack.of(Material.GREEN_CONCRETE_POWDER, 1))); expected.put("green_glazed_terracotta", List.of(ItemStack.of(Material.GREEN_GLAZED_TERRACOTTA, 1))); expected.put("green_shulker_box", List.of(ItemStack.of(Material.GREEN_SHULKER_BOX, 1))); expected.put("green_stained_glass", List.of()); expected.put("green_stained_glass_pane", List.of()); expected.put("green_terracotta", List.of(ItemStack.of(Material.GREEN_TERRACOTTA, 1))); expected.put("green_wool", List.of(ItemStack.of(Material.GREEN_WOOL, 1))); expected.put("grindstone", List.of(ItemStack.of(Material.GRINDSTONE, 1))); expected.put("hanging_roots", List.of()); expected.put("hay_block", List.of(ItemStack.of(Material.HAY_BLOCK, 1))); expected.put("heavy_weighted_pressure_plate", List.of(ItemStack.of(Material.HEAVY_WEIGHTED_PRESSURE_PLATE, 1))); expected.put("honey_block", List.of(ItemStack.of(Material.HONEY_BLOCK, 1))); expected.put("honeycomb_block", List.of(ItemStack.of(Material.HONEYCOMB_BLOCK, 1))); expected.put("hopper", List.of(ItemStack.of(Material.HOPPER, 1))); expected.put("horn_coral", List.of()); expected.put("horn_coral_block", List.of(ItemStack.of(Material.DEAD_HORN_CORAL_BLOCK, 1))); expected.put("horn_coral_fan", List.of()); expected.put("ice", List.of()); expected.put("infested_chiseled_stone_bricks", List.of()); expected.put("infested_cobblestone", List.of()); expected.put("infested_cracked_stone_bricks", List.of()); expected.put("infested_deepslate", List.of()); expected.put("infested_mossy_stone_bricks", List.of()); expected.put("infested_stone", List.of()); expected.put("infested_stone_bricks", List.of()); expected.put("iron_bars", List.of(ItemStack.of(Material.IRON_BARS, 1))); expected.put("iron_block", List.of(ItemStack.of(Material.IRON_BLOCK, 1))); expected.put("iron_door", List.of()); expected.put("iron_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("iron_trapdoor", List.of(ItemStack.of(Material.IRON_TRAPDOOR, 1))); expected.put("jack_o_lantern", List.of(ItemStack.of(Material.JACK_O_LANTERN, 1))); expected.put("jukebox", List.of(ItemStack.of(Material.JUKEBOX, 1))); expected.put("jungle_button", List.of(ItemStack.of(Material.JUNGLE_BUTTON, 1))); expected.put("jungle_door", List.of()); expected.put("jungle_fence", List.of(ItemStack.of(Material.JUNGLE_FENCE, 1))); expected.put("jungle_fence_gate", List.of(ItemStack.of(Material.JUNGLE_FENCE_GATE, 1))); expected.put("jungle_hanging_sign", List.of(ItemStack.of(Material.JUNGLE_HANGING_SIGN, 1))); expected.put("jungle_leaves", List.of()); expected.put("jungle_log", List.of(ItemStack.of(Material.JUNGLE_LOG, 1))); expected.put("jungle_planks", List.of(ItemStack.of(Material.JUNGLE_PLANKS, 1))); expected.put("jungle_pressure_plate", List.of(ItemStack.of(Material.JUNGLE_PRESSURE_PLATE, 1))); expected.put("jungle_sapling", List.of(ItemStack.of(Material.JUNGLE_SAPLING, 1))); expected.put("jungle_sign", List.of(ItemStack.of(Material.JUNGLE_SIGN, 1))); expected.put("jungle_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("jungle_stairs", List.of(ItemStack.of(Material.JUNGLE_STAIRS, 1))); expected.put("jungle_trapdoor", List.of(ItemStack.of(Material.JUNGLE_TRAPDOOR, 1))); expected.put("jungle_wood", List.of(ItemStack.of(Material.JUNGLE_WOOD, 1))); expected.put("kelp", List.of(ItemStack.of(Material.KELP, 1))); expected.put("kelp_plant", List.of(ItemStack.of(Material.KELP, 1))); expected.put("ladder", List.of(ItemStack.of(Material.LADDER, 1))); expected.put("lantern", List.of(ItemStack.of(Material.LANTERN, 1))); expected.put("lapis_block", List.of(ItemStack.of(Material.LAPIS_BLOCK, 1))); expected.put("lapis_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("large_amethyst_bud", List.of()); expected.put("large_fern", List.of()); expected.put("lava_cauldron", List.of(ItemStack.of(Material.CAULDRON, 1))); expected.put("lectern", List.of(ItemStack.of(Material.LECTERN, 1))); expected.put("lever", List.of(ItemStack.of(Material.LEVER, 1))); expected.put("light_blue_banner", List.of(ItemStack.of(Material.LIGHT_BLUE_BANNER, 1))); expected.put("light_blue_bed", List.of()); expected.put("light_blue_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("light_blue_candle_cake", List.of(ItemStack.of(Material.LIGHT_BLUE_CANDLE, 1))); expected.put("light_blue_carpet", List.of(ItemStack.of(Material.LIGHT_BLUE_CARPET, 1))); expected.put("light_blue_concrete", List.of(ItemStack.of(Material.LIGHT_BLUE_CONCRETE, 1))); expected.put("light_blue_concrete_powder", List.of(ItemStack.of(Material.LIGHT_BLUE_CONCRETE_POWDER, 1))); expected.put("light_blue_glazed_terracotta", List.of(ItemStack.of(Material.LIGHT_BLUE_GLAZED_TERRACOTTA, 1))); expected.put("light_blue_shulker_box", List.of(ItemStack.of(Material.LIGHT_BLUE_SHULKER_BOX, 1))); expected.put("light_blue_stained_glass", List.of()); expected.put("light_blue_stained_glass_pane", List.of()); expected.put("light_blue_terracotta", List.of(ItemStack.of(Material.LIGHT_BLUE_TERRACOTTA, 1))); expected.put("light_blue_wool", List.of(ItemStack.of(Material.LIGHT_BLUE_WOOL, 1))); expected.put("light_gray_banner", List.of(ItemStack.of(Material.LIGHT_GRAY_BANNER, 1))); expected.put("light_gray_bed", List.of()); expected.put("light_gray_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("light_gray_candle_cake", List.of(ItemStack.of(Material.LIGHT_GRAY_CANDLE, 1))); expected.put("light_gray_carpet", List.of(ItemStack.of(Material.LIGHT_GRAY_CARPET, 1))); expected.put("light_gray_concrete", List.of(ItemStack.of(Material.LIGHT_GRAY_CONCRETE, 1))); expected.put("light_gray_concrete_powder", List.of(ItemStack.of(Material.LIGHT_GRAY_CONCRETE_POWDER, 1))); expected.put("light_gray_glazed_terracotta", List.of(ItemStack.of(Material.LIGHT_GRAY_GLAZED_TERRACOTTA, 1))); expected.put("light_gray_shulker_box", List.of(ItemStack.of(Material.LIGHT_GRAY_SHULKER_BOX, 1))); expected.put("light_gray_stained_glass", List.of()); expected.put("light_gray_stained_glass_pane", List.of()); expected.put("light_gray_terracotta", List.of(ItemStack.of(Material.LIGHT_GRAY_TERRACOTTA, 1))); expected.put("light_gray_wool", List.of(ItemStack.of(Material.LIGHT_GRAY_WOOL, 1))); expected.put("light_weighted_pressure_plate", List.of(ItemStack.of(Material.LIGHT_WEIGHTED_PRESSURE_PLATE, 1))); expected.put("lightning_rod", List.of(ItemStack.of(Material.LIGHTNING_ROD, 1))); expected.put("lilac", List.of()); expected.put("lily_of_the_valley", List.of(ItemStack.of(Material.LILY_OF_THE_VALLEY, 1))); expected.put("lily_pad", List.of(ItemStack.of(Material.LILY_PAD, 1))); expected.put("lime_banner", List.of(ItemStack.of(Material.LIME_BANNER, 1))); expected.put("lime_bed", List.of()); expected.put("lime_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("lime_candle_cake", List.of(ItemStack.of(Material.LIME_CANDLE, 1))); expected.put("lime_carpet", List.of(ItemStack.of(Material.LIME_CARPET, 1))); expected.put("lime_concrete", List.of(ItemStack.of(Material.LIME_CONCRETE, 1))); expected.put("lime_concrete_powder", List.of(ItemStack.of(Material.LIME_CONCRETE_POWDER, 1))); expected.put("lime_glazed_terracotta", List.of(ItemStack.of(Material.LIME_GLAZED_TERRACOTTA, 1))); expected.put("lime_shulker_box", List.of(ItemStack.of(Material.LIME_SHULKER_BOX, 1))); expected.put("lime_stained_glass", List.of()); expected.put("lime_stained_glass_pane", List.of()); expected.put("lime_terracotta", List.of(ItemStack.of(Material.LIME_TERRACOTTA, 1))); expected.put("lime_wool", List.of(ItemStack.of(Material.LIME_WOOL, 1))); expected.put("lodestone", List.of(ItemStack.of(Material.LODESTONE, 1))); expected.put("loom", List.of(ItemStack.of(Material.LOOM, 1))); expected.put("magenta_banner", List.of(ItemStack.of(Material.MAGENTA_BANNER, 1))); expected.put("magenta_bed", List.of()); expected.put("magenta_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("magenta_candle_cake", List.of(ItemStack.of(Material.MAGENTA_CANDLE, 1))); expected.put("magenta_carpet", List.of(ItemStack.of(Material.MAGENTA_CARPET, 1))); expected.put("magenta_concrete", List.of(ItemStack.of(Material.MAGENTA_CONCRETE, 1))); expected.put("magenta_concrete_powder", List.of(ItemStack.of(Material.MAGENTA_CONCRETE_POWDER, 1))); expected.put("magenta_glazed_terracotta", List.of(ItemStack.of(Material.MAGENTA_GLAZED_TERRACOTTA, 1))); expected.put("magenta_shulker_box", List.of(ItemStack.of(Material.MAGENTA_SHULKER_BOX, 1))); expected.put("magenta_stained_glass", List.of()); expected.put("magenta_stained_glass_pane", List.of()); expected.put("magenta_terracotta", List.of(ItemStack.of(Material.MAGENTA_TERRACOTTA, 1))); expected.put("magenta_wool", List.of(ItemStack.of(Material.MAGENTA_WOOL, 1))); expected.put("magma_block", List.of(ItemStack.of(Material.MAGMA_BLOCK, 1))); expected.put("mangrove_button", List.of(ItemStack.of(Material.MANGROVE_BUTTON, 1))); expected.put("mangrove_door", List.of()); expected.put("mangrove_fence", List.of(ItemStack.of(Material.MANGROVE_FENCE, 1))); expected.put("mangrove_fence_gate", List.of(ItemStack.of(Material.MANGROVE_FENCE_GATE, 1))); expected.put("mangrove_hanging_sign", List.of(ItemStack.of(Material.MANGROVE_HANGING_SIGN, 1))); expected.put("mangrove_leaves", List.of()); expected.put("mangrove_log", List.of(ItemStack.of(Material.MANGROVE_LOG, 1))); expected.put("mangrove_planks", List.of(ItemStack.of(Material.MANGROVE_PLANKS, 1))); expected.put("mangrove_pressure_plate", List.of(ItemStack.of(Material.MANGROVE_PRESSURE_PLATE, 1))); expected.put("mangrove_propagule", List.of()); expected.put("mangrove_roots", List.of(ItemStack.of(Material.MANGROVE_ROOTS, 1))); expected.put("mangrove_sign", List.of(ItemStack.of(Material.MANGROVE_SIGN, 1))); expected.put("mangrove_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("mangrove_stairs", List.of(ItemStack.of(Material.MANGROVE_STAIRS, 1))); expected.put("mangrove_trapdoor", List.of(ItemStack.of(Material.MANGROVE_TRAPDOOR, 1))); expected.put("mangrove_wood", List.of(ItemStack.of(Material.MANGROVE_WOOD, 1))); expected.put("medium_amethyst_bud", List.of()); expected.put("melon", List.of(ItemStack.of(Material.AIR, 1))); expected.put("melon_stem", List.of(ItemStack.of(Material.AIR, 1))); expected.put("moss_block", List.of(ItemStack.of(Material.MOSS_BLOCK, 1))); expected.put("moss_carpet", List.of(ItemStack.of(Material.MOSS_CARPET, 1))); expected.put("mossy_cobblestone", List.of(ItemStack.of(Material.MOSSY_COBBLESTONE, 1))); expected.put("mossy_cobblestone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("mossy_cobblestone_stairs", List.of(ItemStack.of(Material.MOSSY_COBBLESTONE_STAIRS, 1))); expected.put("mossy_cobblestone_wall", List.of(ItemStack.of(Material.MOSSY_COBBLESTONE_WALL, 1))); expected.put("mossy_stone_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("mossy_stone_brick_stairs", List.of(ItemStack.of(Material.MOSSY_STONE_BRICK_STAIRS, 1))); expected.put("mossy_stone_brick_wall", List.of(ItemStack.of(Material.MOSSY_STONE_BRICK_WALL, 1))); expected.put("mossy_stone_bricks", List.of(ItemStack.of(Material.MOSSY_STONE_BRICKS, 1))); expected.put("mud", List.of(ItemStack.of(Material.MUD, 1))); expected.put("mud_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("mud_brick_stairs", List.of(ItemStack.of(Material.MUD_BRICK_STAIRS, 1))); expected.put("mud_brick_wall", List.of(ItemStack.of(Material.MUD_BRICK_WALL, 1))); expected.put("mud_bricks", List.of(ItemStack.of(Material.MUD_BRICKS, 1))); expected.put("muddy_mangrove_roots", List.of(ItemStack.of(Material.MUDDY_MANGROVE_ROOTS, 1))); expected.put("mushroom_stem", List.of()); expected.put("mycelium", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("nether_brick_fence", List.of(ItemStack.of(Material.NETHER_BRICK_FENCE, 1))); expected.put("nether_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("nether_brick_stairs", List.of(ItemStack.of(Material.NETHER_BRICK_STAIRS, 1))); expected.put("nether_brick_wall", List.of(ItemStack.of(Material.NETHER_BRICK_WALL, 1))); expected.put("nether_bricks", List.of(ItemStack.of(Material.NETHER_BRICKS, 1))); expected.put("nether_gold_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("nether_portal", List.of()); expected.put("nether_quartz_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("nether_sprouts", List.of()); expected.put("nether_wart", List.of(ItemStack.of(Material.AIR, 1))); expected.put("nether_wart_block", List.of(ItemStack.of(Material.NETHER_WART_BLOCK, 1))); expected.put("netherite_block", List.of(ItemStack.of(Material.NETHERITE_BLOCK, 1))); expected.put("netherrack", List.of(ItemStack.of(Material.NETHERRACK, 1))); expected.put("note_block", List.of(ItemStack.of(Material.NOTE_BLOCK, 1))); expected.put("oak_button", List.of(ItemStack.of(Material.OAK_BUTTON, 1))); expected.put("oak_door", List.of()); expected.put("oak_fence", List.of(ItemStack.of(Material.OAK_FENCE, 1))); expected.put("oak_fence_gate", List.of(ItemStack.of(Material.OAK_FENCE_GATE, 1))); expected.put("oak_hanging_sign", List.of(ItemStack.of(Material.OAK_HANGING_SIGN, 1))); expected.put("oak_leaves", List.of()); expected.put("oak_log", List.of(ItemStack.of(Material.OAK_LOG, 1))); expected.put("oak_planks", List.of(ItemStack.of(Material.OAK_PLANKS, 1))); expected.put("oak_pressure_plate", List.of(ItemStack.of(Material.OAK_PRESSURE_PLATE, 1))); expected.put("oak_sapling", List.of(ItemStack.of(Material.OAK_SAPLING, 1))); expected.put("oak_sign", List.of(ItemStack.of(Material.OAK_SIGN, 1))); expected.put("oak_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("oak_stairs", List.of(ItemStack.of(Material.OAK_STAIRS, 1))); expected.put("oak_trapdoor", List.of(ItemStack.of(Material.OAK_TRAPDOOR, 1))); expected.put("oak_wood", List.of(ItemStack.of(Material.OAK_WOOD, 1))); expected.put("observer", List.of(ItemStack.of(Material.OBSERVER, 1))); expected.put("obsidian", List.of(ItemStack.of(Material.OBSIDIAN, 1))); expected.put("ochre_froglight", List.of(ItemStack.of(Material.OCHRE_FROGLIGHT, 1))); expected.put("orange_banner", List.of(ItemStack.of(Material.ORANGE_BANNER, 1))); expected.put("orange_bed", List.of()); expected.put("orange_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("orange_candle_cake", List.of(ItemStack.of(Material.ORANGE_CANDLE, 1))); expected.put("orange_carpet", List.of(ItemStack.of(Material.ORANGE_CARPET, 1))); expected.put("orange_concrete", List.of(ItemStack.of(Material.ORANGE_CONCRETE, 1))); expected.put("orange_concrete_powder", List.of(ItemStack.of(Material.ORANGE_CONCRETE_POWDER, 1))); expected.put("orange_glazed_terracotta", List.of(ItemStack.of(Material.ORANGE_GLAZED_TERRACOTTA, 1))); expected.put("orange_shulker_box", List.of(ItemStack.of(Material.ORANGE_SHULKER_BOX, 1))); expected.put("orange_stained_glass", List.of()); expected.put("orange_stained_glass_pane", List.of()); expected.put("orange_terracotta", List.of(ItemStack.of(Material.ORANGE_TERRACOTTA, 1))); expected.put("orange_tulip", List.of(ItemStack.of(Material.ORANGE_TULIP, 1))); expected.put("orange_wool", List.of(ItemStack.of(Material.ORANGE_WOOL, 1))); expected.put("oxeye_daisy", List.of(ItemStack.of(Material.OXEYE_DAISY, 1))); expected.put("oxidized_chiseled_copper", List.of()); expected.put("oxidized_copper", List.of(ItemStack.of(Material.OXIDIZED_COPPER, 1))); expected.put("oxidized_copper_bulb", List.of()); expected.put("oxidized_copper_door", List.of()); expected.put("oxidized_copper_grate", List.of()); expected.put("oxidized_copper_trapdoor", List.of()); expected.put("oxidized_cut_copper", List.of(ItemStack.of(Material.OXIDIZED_CUT_COPPER, 1))); expected.put("oxidized_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("oxidized_cut_copper_stairs", List.of(ItemStack.of(Material.OXIDIZED_CUT_COPPER_STAIRS, 1))); expected.put("packed_ice", List.of()); expected.put("packed_mud", List.of(ItemStack.of(Material.PACKED_MUD, 1))); expected.put("pearlescent_froglight", List.of(ItemStack.of(Material.PEARLESCENT_FROGLIGHT, 1))); expected.put("peony", List.of()); expected.put("petrified_oak_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("piglin_head", List.of(ItemStack.of(Material.PIGLIN_HEAD, 1))); expected.put("pink_banner", List.of(ItemStack.of(Material.PINK_BANNER, 1))); expected.put("pink_bed", List.of()); expected.put("pink_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("pink_candle_cake", List.of(ItemStack.of(Material.PINK_CANDLE, 1))); expected.put("pink_carpet", List.of(ItemStack.of(Material.PINK_CARPET, 1))); expected.put("pink_concrete", List.of(ItemStack.of(Material.PINK_CONCRETE, 1))); expected.put("pink_concrete_powder", List.of(ItemStack.of(Material.PINK_CONCRETE_POWDER, 1))); expected.put("pink_glazed_terracotta", List.of(ItemStack.of(Material.PINK_GLAZED_TERRACOTTA, 1))); expected.put("pink_petals", List.of(ItemStack.of(Material.AIR, 1))); expected.put("pink_shulker_box", List.of(ItemStack.of(Material.PINK_SHULKER_BOX, 1))); expected.put("pink_stained_glass", List.of()); expected.put("pink_stained_glass_pane", List.of()); expected.put("pink_terracotta", List.of(ItemStack.of(Material.PINK_TERRACOTTA, 1))); expected.put("pink_tulip", List.of(ItemStack.of(Material.PINK_TULIP, 1))); expected.put("pink_wool", List.of(ItemStack.of(Material.PINK_WOOL, 1))); expected.put("piston", List.of(ItemStack.of(Material.PISTON, 1))); expected.put("pitcher_crop", List.of()); expected.put("pitcher_plant", List.of()); expected.put("player_head", List.of(ItemStack.of(Material.PLAYER_HEAD, 1))); expected.put("podzol", List.of(ItemStack.of(Material.DIRT, 1))); expected.put("pointed_dripstone", List.of(ItemStack.of(Material.POINTED_DRIPSTONE, 1))); expected.put("polished_andesite", List.of(ItemStack.of(Material.POLISHED_ANDESITE, 1))); expected.put("polished_andesite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_andesite_stairs", List.of(ItemStack.of(Material.POLISHED_ANDESITE_STAIRS, 1))); expected.put("polished_basalt", List.of(ItemStack.of(Material.POLISHED_BASALT, 1))); expected.put("polished_blackstone", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE, 1))); expected.put("polished_blackstone_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_blackstone_brick_stairs", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_BRICK_STAIRS, 1))); expected.put("polished_blackstone_brick_wall", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_BRICK_WALL, 1))); expected.put("polished_blackstone_bricks", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_BRICKS, 1))); expected.put("polished_blackstone_button", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_BUTTON, 1))); expected.put("polished_blackstone_pressure_plate", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_PRESSURE_PLATE, 1))); expected.put("polished_blackstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_blackstone_stairs", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_STAIRS, 1))); expected.put("polished_blackstone_wall", List.of(ItemStack.of(Material.POLISHED_BLACKSTONE_WALL, 1))); expected.put("polished_deepslate", List.of(ItemStack.of(Material.POLISHED_DEEPSLATE, 1))); expected.put("polished_deepslate_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_deepslate_stairs", List.of(ItemStack.of(Material.POLISHED_DEEPSLATE_STAIRS, 1))); expected.put("polished_deepslate_wall", List.of(ItemStack.of(Material.POLISHED_DEEPSLATE_WALL, 1))); expected.put("polished_diorite", List.of(ItemStack.of(Material.POLISHED_DIORITE, 1))); expected.put("polished_diorite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_diorite_stairs", List.of(ItemStack.of(Material.POLISHED_DIORITE_STAIRS, 1))); expected.put("polished_granite", List.of(ItemStack.of(Material.POLISHED_GRANITE, 1))); expected.put("polished_granite_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("polished_granite_stairs", List.of(ItemStack.of(Material.POLISHED_GRANITE_STAIRS, 1))); expected.put("polished_tuff", List.of()); expected.put("polished_tuff_slab", List.of()); expected.put("polished_tuff_stairs", List.of()); expected.put("polished_tuff_wall", List.of()); expected.put("poppy", List.of(ItemStack.of(Material.POPPY, 1))); expected.put("potatoes", List.of(ItemStack.of(Material.AIR, 1))); expected.put("potted_acacia_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.ACACIA_SAPLING, 1))); expected.put("potted_allium", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.ALLIUM, 1))); expected.put("potted_azalea_bush", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.AZALEA, 1))); expected.put("potted_azure_bluet", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.AZURE_BLUET, 1))); expected.put("potted_bamboo", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.BAMBOO, 1))); expected.put("potted_birch_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.BIRCH_SAPLING, 1))); expected.put("potted_blue_orchid", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.BLUE_ORCHID, 1))); expected.put("potted_brown_mushroom", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.BROWN_MUSHROOM, 1))); expected.put("potted_cactus", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.CACTUS, 1))); expected.put("potted_cherry_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.CHERRY_SAPLING, 1))); expected.put("potted_cornflower", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.CORNFLOWER, 1))); expected.put("potted_crimson_fungus", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.CRIMSON_FUNGUS, 1))); expected.put("potted_crimson_roots", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.CRIMSON_ROOTS, 1))); expected.put("potted_dandelion", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.DANDELION, 1))); expected.put("potted_dark_oak_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.DARK_OAK_SAPLING, 1))); expected.put("potted_dead_bush", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.DEAD_BUSH, 1))); expected.put("potted_fern", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.FERN, 1))); expected.put("potted_flowering_azalea_bush", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.FLOWERING_AZALEA, 1))); expected.put("potted_jungle_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.JUNGLE_SAPLING, 1))); expected.put("potted_lily_of_the_valley", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.LILY_OF_THE_VALLEY, 1))); expected.put("potted_mangrove_propagule", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.MANGROVE_PROPAGULE, 1))); expected.put("potted_oak_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.OAK_SAPLING, 1))); expected.put("potted_orange_tulip", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.ORANGE_TULIP, 1))); expected.put("potted_oxeye_daisy", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.OXEYE_DAISY, 1))); expected.put("potted_pink_tulip", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.PINK_TULIP, 1))); expected.put("potted_poppy", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.POPPY, 1))); expected.put("potted_red_mushroom", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.RED_MUSHROOM, 1))); expected.put("potted_red_tulip", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.RED_TULIP, 1))); expected.put("potted_spruce_sapling", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.SPRUCE_SAPLING, 1))); expected.put("potted_torchflower", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.TORCHFLOWER, 1))); expected.put("potted_warped_fungus", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.WARPED_FUNGUS, 1))); expected.put("potted_warped_roots", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.WARPED_ROOTS, 1))); expected.put("potted_white_tulip", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.WHITE_TULIP, 1))); expected.put("potted_wither_rose", List.of(ItemStack.of(Material.FLOWER_POT, 1), ItemStack.of(Material.WITHER_ROSE, 1))); expected.put("powder_snow", List.of()); expected.put("powder_snow_cauldron", List.of(ItemStack.of(Material.CAULDRON, 1))); expected.put("powered_rail", List.of(ItemStack.of(Material.POWERED_RAIL, 1))); expected.put("prismarine", List.of(ItemStack.of(Material.PRISMARINE, 1))); expected.put("prismarine_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("prismarine_brick_stairs", List.of(ItemStack.of(Material.PRISMARINE_BRICK_STAIRS, 1))); expected.put("prismarine_bricks", List.of(ItemStack.of(Material.PRISMARINE_BRICKS, 1))); expected.put("prismarine_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("prismarine_stairs", List.of(ItemStack.of(Material.PRISMARINE_STAIRS, 1))); expected.put("prismarine_wall", List.of(ItemStack.of(Material.PRISMARINE_WALL, 1))); expected.put("pumpkin", List.of(ItemStack.of(Material.PUMPKIN, 1))); expected.put("pumpkin_stem", List.of(ItemStack.of(Material.AIR, 1))); expected.put("purple_banner", List.of(ItemStack.of(Material.PURPLE_BANNER, 1))); expected.put("purple_bed", List.of()); expected.put("purple_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("purple_candle_cake", List.of(ItemStack.of(Material.PURPLE_CANDLE, 1))); expected.put("purple_carpet", List.of(ItemStack.of(Material.PURPLE_CARPET, 1))); expected.put("purple_concrete", List.of(ItemStack.of(Material.PURPLE_CONCRETE, 1))); expected.put("purple_concrete_powder", List.of(ItemStack.of(Material.PURPLE_CONCRETE_POWDER, 1))); expected.put("purple_glazed_terracotta", List.of(ItemStack.of(Material.PURPLE_GLAZED_TERRACOTTA, 1))); expected.put("purple_shulker_box", List.of(ItemStack.of(Material.PURPLE_SHULKER_BOX, 1))); expected.put("purple_stained_glass", List.of()); expected.put("purple_stained_glass_pane", List.of()); expected.put("purple_terracotta", List.of(ItemStack.of(Material.PURPLE_TERRACOTTA, 1))); expected.put("purple_wool", List.of(ItemStack.of(Material.PURPLE_WOOL, 1))); expected.put("purpur_block", List.of(ItemStack.of(Material.PURPUR_BLOCK, 1))); expected.put("purpur_pillar", List.of(ItemStack.of(Material.PURPUR_PILLAR, 1))); expected.put("purpur_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("purpur_stairs", List.of(ItemStack.of(Material.PURPUR_STAIRS, 1))); expected.put("quartz_block", List.of(ItemStack.of(Material.QUARTZ_BLOCK, 1))); expected.put("quartz_bricks", List.of(ItemStack.of(Material.QUARTZ_BRICKS, 1))); expected.put("quartz_pillar", List.of(ItemStack.of(Material.QUARTZ_PILLAR, 1))); expected.put("quartz_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("quartz_stairs", List.of(ItemStack.of(Material.QUARTZ_STAIRS, 1))); expected.put("rail", List.of(ItemStack.of(Material.RAIL, 1))); expected.put("raw_copper_block", List.of(ItemStack.of(Material.RAW_COPPER_BLOCK, 1))); expected.put("raw_gold_block", List.of(ItemStack.of(Material.RAW_GOLD_BLOCK, 1))); expected.put("raw_iron_block", List.of(ItemStack.of(Material.RAW_IRON_BLOCK, 1))); expected.put("red_banner", List.of(ItemStack.of(Material.RED_BANNER, 1))); expected.put("red_bed", List.of()); expected.put("red_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("red_candle_cake", List.of(ItemStack.of(Material.RED_CANDLE, 1))); expected.put("red_carpet", List.of(ItemStack.of(Material.RED_CARPET, 1))); expected.put("red_concrete", List.of(ItemStack.of(Material.RED_CONCRETE, 1))); expected.put("red_concrete_powder", List.of(ItemStack.of(Material.RED_CONCRETE_POWDER, 1))); expected.put("red_glazed_terracotta", List.of(ItemStack.of(Material.RED_GLAZED_TERRACOTTA, 1))); expected.put("red_mushroom", List.of(ItemStack.of(Material.RED_MUSHROOM, 1))); expected.put("red_mushroom_block", List.of(ItemStack.of(Material.AIR, 1))); expected.put("red_nether_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("red_nether_brick_stairs", List.of(ItemStack.of(Material.RED_NETHER_BRICK_STAIRS, 1))); expected.put("red_nether_brick_wall", List.of(ItemStack.of(Material.RED_NETHER_BRICK_WALL, 1))); expected.put("red_nether_bricks", List.of(ItemStack.of(Material.RED_NETHER_BRICKS, 1))); expected.put("red_sand", List.of(ItemStack.of(Material.RED_SAND, 1))); expected.put("red_sandstone", List.of(ItemStack.of(Material.RED_SANDSTONE, 1))); expected.put("red_sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("red_sandstone_stairs", List.of(ItemStack.of(Material.RED_SANDSTONE_STAIRS, 1))); expected.put("red_sandstone_wall", List.of(ItemStack.of(Material.RED_SANDSTONE_WALL, 1))); expected.put("red_shulker_box", List.of(ItemStack.of(Material.RED_SHULKER_BOX, 1))); expected.put("red_stained_glass", List.of()); expected.put("red_stained_glass_pane", List.of()); expected.put("red_terracotta", List.of(ItemStack.of(Material.RED_TERRACOTTA, 1))); expected.put("red_tulip", List.of(ItemStack.of(Material.RED_TULIP, 1))); expected.put("red_wool", List.of(ItemStack.of(Material.RED_WOOL, 1))); expected.put("redstone_block", List.of(ItemStack.of(Material.REDSTONE_BLOCK, 1))); expected.put("redstone_lamp", List.of(ItemStack.of(Material.REDSTONE_LAMP, 1))); expected.put("redstone_ore", List.of(ItemStack.of(Material.AIR, 1))); expected.put("redstone_torch", List.of(ItemStack.of(Material.REDSTONE_TORCH, 1))); expected.put("redstone_wire", List.of(ItemStack.of(Material.REDSTONE, 1))); expected.put("reinforced_deepslate", List.of()); expected.put("repeater", List.of(ItemStack.of(Material.REPEATER, 1))); expected.put("respawn_anchor", List.of(ItemStack.of(Material.RESPAWN_ANCHOR, 1))); expected.put("rooted_dirt", List.of(ItemStack.of(Material.ROOTED_DIRT, 1))); expected.put("rose_bush", List.of()); expected.put("sand", List.of(ItemStack.of(Material.SAND, 1))); expected.put("sandstone", List.of(ItemStack.of(Material.SANDSTONE, 1))); expected.put("sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("sandstone_stairs", List.of(ItemStack.of(Material.SANDSTONE_STAIRS, 1))); expected.put("sandstone_wall", List.of(ItemStack.of(Material.SANDSTONE_WALL, 1))); expected.put("scaffolding", List.of(ItemStack.of(Material.SCAFFOLDING, 1))); expected.put("sculk", List.of()); expected.put("sculk_catalyst", List.of()); expected.put("sculk_sensor", List.of()); expected.put("sculk_shrieker", List.of()); expected.put("sculk_vein", List.of()); expected.put("sea_lantern", List.of(ItemStack.of(Material.AIR, 1))); expected.put("sea_pickle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("seagrass", List.of()); expected.put("short_grass", List.of()); expected.put("shroomlight", List.of(ItemStack.of(Material.SHROOMLIGHT, 1))); expected.put("shulker_box", List.of(ItemStack.of(Material.SHULKER_BOX, 1))); expected.put("skeleton_skull", List.of(ItemStack.of(Material.SKELETON_SKULL, 1))); expected.put("slime_block", List.of(ItemStack.of(Material.SLIME_BLOCK, 1))); expected.put("small_amethyst_bud", List.of()); expected.put("small_dripleaf", List.of()); expected.put("smithing_table", List.of(ItemStack.of(Material.SMITHING_TABLE, 1))); expected.put("smoker", List.of(ItemStack.of(Material.SMOKER, 1))); expected.put("smooth_basalt", List.of(ItemStack.of(Material.SMOOTH_BASALT, 1))); expected.put("smooth_quartz", List.of(ItemStack.of(Material.SMOOTH_QUARTZ, 1))); expected.put("smooth_quartz_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("smooth_quartz_stairs", List.of(ItemStack.of(Material.SMOOTH_QUARTZ_STAIRS, 1))); expected.put("smooth_red_sandstone", List.of(ItemStack.of(Material.SMOOTH_RED_SANDSTONE, 1))); expected.put("smooth_red_sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("smooth_red_sandstone_stairs", List.of(ItemStack.of(Material.SMOOTH_RED_SANDSTONE_STAIRS, 1))); expected.put("smooth_sandstone", List.of(ItemStack.of(Material.SMOOTH_SANDSTONE, 1))); expected.put("smooth_sandstone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("smooth_sandstone_stairs", List.of(ItemStack.of(Material.SMOOTH_SANDSTONE_STAIRS, 1))); expected.put("smooth_stone", List.of(ItemStack.of(Material.SMOOTH_STONE, 1))); expected.put("smooth_stone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("sniffer_egg", List.of(ItemStack.of(Material.SNIFFER_EGG, 1))); expected.put("snow", List.of()); expected.put("snow_block", List.of(ItemStack.of(Material.AIR, 1))); expected.put("soul_campfire", List.of(ItemStack.of(Material.SOUL_SOIL, 1))); expected.put("soul_fire", List.of()); expected.put("soul_lantern", List.of(ItemStack.of(Material.SOUL_LANTERN, 1))); expected.put("soul_sand", List.of(ItemStack.of(Material.SOUL_SAND, 1))); expected.put("soul_soil", List.of(ItemStack.of(Material.SOUL_SOIL, 1))); expected.put("soul_torch", List.of(ItemStack.of(Material.SOUL_TORCH, 1))); expected.put("spawner", List.of()); expected.put("sponge", List.of(ItemStack.of(Material.SPONGE, 1))); expected.put("spore_blossom", List.of(ItemStack.of(Material.SPORE_BLOSSOM, 1))); expected.put("spruce_button", List.of(ItemStack.of(Material.SPRUCE_BUTTON, 1))); expected.put("spruce_door", List.of()); expected.put("spruce_fence", List.of(ItemStack.of(Material.SPRUCE_FENCE, 1))); expected.put("spruce_fence_gate", List.of(ItemStack.of(Material.SPRUCE_FENCE_GATE, 1))); expected.put("spruce_hanging_sign", List.of(ItemStack.of(Material.SPRUCE_HANGING_SIGN, 1))); expected.put("spruce_leaves", List.of()); expected.put("spruce_log", List.of(ItemStack.of(Material.SPRUCE_LOG, 1))); expected.put("spruce_planks", List.of(ItemStack.of(Material.SPRUCE_PLANKS, 1))); expected.put("spruce_pressure_plate", List.of(ItemStack.of(Material.SPRUCE_PRESSURE_PLATE, 1))); expected.put("spruce_sapling", List.of(ItemStack.of(Material.SPRUCE_SAPLING, 1))); expected.put("spruce_sign", List.of(ItemStack.of(Material.SPRUCE_SIGN, 1))); expected.put("spruce_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("spruce_stairs", List.of(ItemStack.of(Material.SPRUCE_STAIRS, 1))); expected.put("spruce_trapdoor", List.of(ItemStack.of(Material.SPRUCE_TRAPDOOR, 1))); expected.put("spruce_wood", List.of(ItemStack.of(Material.SPRUCE_WOOD, 1))); expected.put("sticky_piston", List.of(ItemStack.of(Material.STICKY_PISTON, 1))); expected.put("stone", List.of(ItemStack.of(Material.COBBLESTONE, 1))); expected.put("stone_brick_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("stone_brick_stairs", List.of(ItemStack.of(Material.STONE_BRICK_STAIRS, 1))); expected.put("stone_brick_wall", List.of(ItemStack.of(Material.STONE_BRICK_WALL, 1))); expected.put("stone_bricks", List.of(ItemStack.of(Material.STONE_BRICKS, 1))); expected.put("stone_button", List.of(ItemStack.of(Material.STONE_BUTTON, 1))); expected.put("stone_pressure_plate", List.of(ItemStack.of(Material.STONE_PRESSURE_PLATE, 1))); expected.put("stone_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("stone_stairs", List.of(ItemStack.of(Material.STONE_STAIRS, 1))); expected.put("stonecutter", List.of(ItemStack.of(Material.STONECUTTER, 1))); expected.put("stripped_acacia_log", List.of(ItemStack.of(Material.STRIPPED_ACACIA_LOG, 1))); expected.put("stripped_acacia_wood", List.of(ItemStack.of(Material.STRIPPED_ACACIA_WOOD, 1))); expected.put("stripped_bamboo_block", List.of(ItemStack.of(Material.STRIPPED_BAMBOO_BLOCK, 1))); expected.put("stripped_birch_log", List.of(ItemStack.of(Material.STRIPPED_BIRCH_LOG, 1))); expected.put("stripped_birch_wood", List.of(ItemStack.of(Material.STRIPPED_BIRCH_WOOD, 1))); expected.put("stripped_cherry_log", List.of(ItemStack.of(Material.STRIPPED_CHERRY_LOG, 1))); expected.put("stripped_cherry_wood", List.of(ItemStack.of(Material.STRIPPED_CHERRY_WOOD, 1))); expected.put("stripped_crimson_hyphae", List.of(ItemStack.of(Material.STRIPPED_CRIMSON_HYPHAE, 1))); expected.put("stripped_crimson_stem", List.of(ItemStack.of(Material.STRIPPED_CRIMSON_STEM, 1))); expected.put("stripped_dark_oak_log", List.of(ItemStack.of(Material.STRIPPED_DARK_OAK_LOG, 1))); expected.put("stripped_dark_oak_wood", List.of(ItemStack.of(Material.STRIPPED_DARK_OAK_WOOD, 1))); expected.put("stripped_jungle_log", List.of(ItemStack.of(Material.STRIPPED_JUNGLE_LOG, 1))); expected.put("stripped_jungle_wood", List.of(ItemStack.of(Material.STRIPPED_JUNGLE_WOOD, 1))); expected.put("stripped_mangrove_log", List.of(ItemStack.of(Material.STRIPPED_MANGROVE_LOG, 1))); expected.put("stripped_mangrove_wood", List.of(ItemStack.of(Material.STRIPPED_MANGROVE_WOOD, 1))); expected.put("stripped_oak_log", List.of(ItemStack.of(Material.STRIPPED_OAK_LOG, 1))); expected.put("stripped_oak_wood", List.of(ItemStack.of(Material.STRIPPED_OAK_WOOD, 1))); expected.put("stripped_spruce_log", List.of(ItemStack.of(Material.STRIPPED_SPRUCE_LOG, 1))); expected.put("stripped_spruce_wood", List.of(ItemStack.of(Material.STRIPPED_SPRUCE_WOOD, 1))); expected.put("stripped_warped_hyphae", List.of(ItemStack.of(Material.STRIPPED_WARPED_HYPHAE, 1))); expected.put("stripped_warped_stem", List.of(ItemStack.of(Material.STRIPPED_WARPED_STEM, 1))); expected.put("sugar_cane", List.of(ItemStack.of(Material.SUGAR_CANE, 1))); expected.put("sunflower", List.of()); expected.put("suspicious_gravel", List.of()); expected.put("suspicious_sand", List.of()); expected.put("sweet_berry_bush", List.of()); expected.put("tall_grass", List.of()); expected.put("tall_seagrass", List.of()); expected.put("target", List.of(ItemStack.of(Material.TARGET, 1))); expected.put("terracotta", List.of(ItemStack.of(Material.TERRACOTTA, 1))); expected.put("tinted_glass", List.of(ItemStack.of(Material.TINTED_GLASS, 1))); expected.put("tnt", List.of()); expected.put("torch", List.of(ItemStack.of(Material.TORCH, 1))); expected.put("torchflower", List.of(ItemStack.of(Material.TORCHFLOWER, 1))); expected.put("torchflower_crop", List.of(ItemStack.of(Material.AIR, 1))); expected.put("trapped_chest", List.of(ItemStack.of(Material.TRAPPED_CHEST, 1))); expected.put("trial_spawner", List.of()); expected.put("tripwire", List.of(ItemStack.of(Material.STRING, 1))); expected.put("tripwire_hook", List.of(ItemStack.of(Material.TRIPWIRE_HOOK, 1))); expected.put("tube_coral", List.of()); expected.put("tube_coral_block", List.of(ItemStack.of(Material.DEAD_TUBE_CORAL_BLOCK, 1))); expected.put("tube_coral_fan", List.of()); expected.put("tuff", List.of(ItemStack.of(Material.TUFF, 1))); expected.put("tuff_brick_slab", List.of()); expected.put("tuff_brick_stairs", List.of()); expected.put("tuff_brick_wall", List.of()); expected.put("tuff_bricks", List.of()); expected.put("tuff_slab", List.of()); expected.put("tuff_stairs", List.of()); expected.put("tuff_wall", List.of()); expected.put("turtle_egg", List.of()); expected.put("twisting_vines", List.of()); expected.put("twisting_vines_plant", List.of()); expected.put("verdant_froglight", List.of(ItemStack.of(Material.VERDANT_FROGLIGHT, 1))); expected.put("vine", List.of()); expected.put("warped_button", List.of(ItemStack.of(Material.WARPED_BUTTON, 1))); expected.put("warped_door", List.of()); expected.put("warped_fence", List.of(ItemStack.of(Material.WARPED_FENCE, 1))); expected.put("warped_fence_gate", List.of(ItemStack.of(Material.WARPED_FENCE_GATE, 1))); expected.put("warped_fungus", List.of(ItemStack.of(Material.WARPED_FUNGUS, 1))); expected.put("warped_hanging_sign", List.of(ItemStack.of(Material.WARPED_HANGING_SIGN, 1))); expected.put("warped_hyphae", List.of(ItemStack.of(Material.WARPED_HYPHAE, 1))); expected.put("warped_nylium", List.of(ItemStack.of(Material.NETHERRACK, 1))); expected.put("warped_planks", List.of(ItemStack.of(Material.WARPED_PLANKS, 1))); expected.put("warped_pressure_plate", List.of(ItemStack.of(Material.WARPED_PRESSURE_PLATE, 1))); expected.put("warped_roots", List.of(ItemStack.of(Material.WARPED_ROOTS, 1))); expected.put("warped_sign", List.of(ItemStack.of(Material.WARPED_SIGN, 1))); expected.put("warped_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("warped_stairs", List.of(ItemStack.of(Material.WARPED_STAIRS, 1))); expected.put("warped_stem", List.of(ItemStack.of(Material.WARPED_STEM, 1))); expected.put("warped_trapdoor", List.of(ItemStack.of(Material.WARPED_TRAPDOOR, 1))); expected.put("warped_wart_block", List.of(ItemStack.of(Material.WARPED_WART_BLOCK, 1))); expected.put("water_cauldron", List.of(ItemStack.of(Material.CAULDRON, 1))); expected.put("waxed_chiseled_copper", List.of()); expected.put("waxed_copper_block", List.of(ItemStack.of(Material.WAXED_COPPER_BLOCK, 1))); expected.put("waxed_copper_bulb", List.of()); expected.put("waxed_copper_door", List.of()); expected.put("waxed_copper_grate", List.of()); expected.put("waxed_copper_trapdoor", List.of()); expected.put("waxed_cut_copper", List.of(ItemStack.of(Material.WAXED_CUT_COPPER, 1))); expected.put("waxed_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("waxed_cut_copper_stairs", List.of(ItemStack.of(Material.WAXED_CUT_COPPER_STAIRS, 1))); expected.put("waxed_exposed_chiseled_copper", List.of()); expected.put("waxed_exposed_copper", List.of(ItemStack.of(Material.WAXED_EXPOSED_COPPER, 1))); expected.put("waxed_exposed_copper_bulb", List.of()); expected.put("waxed_exposed_copper_door", List.of()); expected.put("waxed_exposed_copper_grate", List.of()); expected.put("waxed_exposed_copper_trapdoor", List.of()); expected.put("waxed_exposed_cut_copper", List.of(ItemStack.of(Material.WAXED_EXPOSED_CUT_COPPER, 1))); expected.put("waxed_exposed_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("waxed_exposed_cut_copper_stairs", List.of(ItemStack.of(Material.WAXED_EXPOSED_CUT_COPPER_STAIRS, 1))); expected.put("waxed_oxidized_chiseled_copper", List.of()); expected.put("waxed_oxidized_copper", List.of(ItemStack.of(Material.WAXED_OXIDIZED_COPPER, 1))); expected.put("waxed_oxidized_copper_bulb", List.of()); expected.put("waxed_oxidized_copper_door", List.of()); expected.put("waxed_oxidized_copper_grate", List.of()); expected.put("waxed_oxidized_copper_trapdoor", List.of()); expected.put("waxed_oxidized_cut_copper", List.of(ItemStack.of(Material.WAXED_OXIDIZED_CUT_COPPER, 1))); expected.put("waxed_oxidized_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("waxed_oxidized_cut_copper_stairs", List.of(ItemStack.of(Material.WAXED_OXIDIZED_CUT_COPPER_STAIRS, 1))); expected.put("waxed_weathered_chiseled_copper", List.of()); expected.put("waxed_weathered_copper", List.of(ItemStack.of(Material.WAXED_WEATHERED_COPPER, 1))); expected.put("waxed_weathered_copper_bulb", List.of()); expected.put("waxed_weathered_copper_door", List.of()); expected.put("waxed_weathered_copper_grate", List.of()); expected.put("waxed_weathered_copper_trapdoor", List.of()); expected.put("waxed_weathered_cut_copper", List.of(ItemStack.of(Material.WAXED_WEATHERED_CUT_COPPER, 1))); expected.put("waxed_weathered_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("waxed_weathered_cut_copper_stairs", List.of(ItemStack.of(Material.WAXED_WEATHERED_CUT_COPPER_STAIRS, 1))); expected.put("weathered_chiseled_copper", List.of()); expected.put("weathered_copper", List.of(ItemStack.of(Material.WEATHERED_COPPER, 1))); expected.put("weathered_copper_bulb", List.of()); expected.put("weathered_copper_door", List.of()); expected.put("weathered_copper_grate", List.of()); expected.put("weathered_copper_trapdoor", List.of()); expected.put("weathered_cut_copper", List.of(ItemStack.of(Material.WEATHERED_CUT_COPPER, 1))); expected.put("weathered_cut_copper_slab", List.of(ItemStack.of(Material.AIR, 1))); expected.put("weathered_cut_copper_stairs", List.of(ItemStack.of(Material.WEATHERED_CUT_COPPER_STAIRS, 1))); expected.put("weeping_vines", List.of()); expected.put("weeping_vines_plant", List.of()); expected.put("wet_sponge", List.of(ItemStack.of(Material.WET_SPONGE, 1))); expected.put("wheat", List.of(ItemStack.of(Material.AIR, 1))); expected.put("white_banner", List.of(ItemStack.of(Material.WHITE_BANNER, 1))); expected.put("white_bed", List.of()); expected.put("white_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("white_candle_cake", List.of(ItemStack.of(Material.WHITE_CANDLE, 1))); expected.put("white_carpet", List.of(ItemStack.of(Material.WHITE_CARPET, 1))); expected.put("white_concrete", List.of(ItemStack.of(Material.WHITE_CONCRETE, 1))); expected.put("white_concrete_powder", List.of(ItemStack.of(Material.WHITE_CONCRETE_POWDER, 1))); expected.put("white_glazed_terracotta", List.of(ItemStack.of(Material.WHITE_GLAZED_TERRACOTTA, 1))); expected.put("white_shulker_box", List.of(ItemStack.of(Material.WHITE_SHULKER_BOX, 1))); expected.put("white_stained_glass", List.of()); expected.put("white_stained_glass_pane", List.of()); expected.put("white_terracotta", List.of(ItemStack.of(Material.WHITE_TERRACOTTA, 1))); expected.put("white_tulip", List.of(ItemStack.of(Material.WHITE_TULIP, 1))); expected.put("white_wool", List.of(ItemStack.of(Material.WHITE_WOOL, 1))); expected.put("wither_rose", List.of(ItemStack.of(Material.WITHER_ROSE, 1))); expected.put("wither_skeleton_skull", List.of(ItemStack.of(Material.WITHER_SKELETON_SKULL, 1))); expected.put("yellow_banner", List.of(ItemStack.of(Material.YELLOW_BANNER, 1))); expected.put("yellow_bed", List.of()); expected.put("yellow_candle", List.of(ItemStack.of(Material.AIR, 1))); expected.put("yellow_candle_cake", List.of(ItemStack.of(Material.YELLOW_CANDLE, 1))); expected.put("yellow_carpet", List.of(ItemStack.of(Material.YELLOW_CARPET, 1))); expected.put("yellow_concrete", List.of(ItemStack.of(Material.YELLOW_CONCRETE, 1))); expected.put("yellow_concrete_powder", List.of(ItemStack.of(Material.YELLOW_CONCRETE_POWDER, 1))); expected.put("yellow_glazed_terracotta", List.of(ItemStack.of(Material.YELLOW_GLAZED_TERRACOTTA, 1))); expected.put("yellow_shulker_box", List.of(ItemStack.of(Material.YELLOW_SHULKER_BOX, 1))); expected.put("yellow_stained_glass", List.of()); expected.put("yellow_stained_glass_pane", List.of()); expected.put("yellow_terracotta", List.of(ItemStack.of(Material.YELLOW_TERRACOTTA, 1))); expected.put("yellow_wool", List.of(ItemStack.of(Material.YELLOW_WOOL, 1))); expected.put("zombie_head", List.of(ItemStack.of(Material.ZOMBIE_HEAD, 1))); EXPECTED_RESULTS = Map.copyOf(expected); } public static final Map, Object>> TRAITS; static { Map, Object>> traits = new HashMap<>(); // acacia_leaves : tool traits.put("acacia_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // acacia_slab : explosion radius traits.put("acacia_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // amethyst_cluster : explosion radius traits.put("amethyst_cluster", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // andesite_slab : explosion radius traits.put("andesite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // attached_pumpkin_stem : explosion_radius traits.put("attached_pumpkin_stem", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // azalea_leaves : tool traits.put("azalea_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // bamboo_mosaic_slab : explosion_radius traits.put("bamboo_mosaic_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // bamboo_slab : explosion_radius traits.put("bamboo_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // barrel : block_entity traits.put("barrel", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // beacon : block_entity traits.put("beacon", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // beetroots : explosion_radius traits.put("beetroots", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // birch_leaves : tool traits.put("birch_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // birch_slab : explosion_radius traits.put("birch_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // black_banner : block_entity traits.put("black_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // black_candle : explosion_radius traits.put("black_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // black_shulker_box : block_entity traits.put("black_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // blackstone_slab : explosion_radius traits.put("blackstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // blast_furnace : block_entity traits.put("blast_furnace", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // blue_banner : block_entity traits.put("blue_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // blue_candle : explosion_radius traits.put("blue_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // blue_shulker_box : block_entity traits.put("blue_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // bookshelf : explosion_radius traits.put("bookshelf", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // brewing_stand : block_entity traits.put("brewing_stand", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // brick_slab : explosion_radius traits.put("brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // brown_banner : block_entity traits.put("brown_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // brown_candle : explosion_radius traits.put("brown_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // brown_mushroom : explosion_radius traits.put("brown_mushroom_block", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // brown_shulker_box : block_entity traits.put("brown_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // candle : explosion_radius traits.put("candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // carrots : explosion_radius traits.put("carrots", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cherry_leaves : tool traits.put("cherry_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // cherry_slab : explosion_radius traits.put("cherry_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // chest : block_entity traits.put("chest", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // clay : explosion_radius traits.put("clay", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // coal_ore : explosion_radius traits.put("coal_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cobbled_deepslate_slab : explosion_radius traits.put("cobbled_deepslate_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cobblestone_slab : explosion_radius traits.put("cobblestone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cocoa : explosion_radius traits.put("cocoa", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // composter : explosion_radius traits.put("composter", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // copper_ore : explosion_radius traits.put("copper_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // crimson_slab : explosion_radius traits.put("crimson_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cut_copper_slab : explosion_radius traits.put("cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cut_red_sandstone_slab : explosion_radius traits.put("cut_red_sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cut_sandstone_slab : explosion_radius traits.put("cut_sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cyan_banner : block_entity traits.put("cyan_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // cyan_candle : explosion_radius traits.put("cyan_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // cyan_shulker_box : block_entity traits.put("cyan_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // dark_oak_leaves : tool traits.put("dark_oak_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // dark_oak_slab : explosion_radius traits.put("dark_oak_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // dark_prismarine_slab : explosion_radius traits.put("dark_prismarine_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // decorated_pot : block_entity traits.put("decorated_pot", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // deepslate_brick_slab : explosion_radius traits.put("deepslate_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_coal_ore : explosion_radius traits.put("deepslate_coal_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_copper_ore : explosion_radius traits.put("deepslate_copper_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_diamond_ore : explosion_radius traits.put("deepslate_diamond_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_emerald_ore : explosion_radius traits.put("deepslate_emerald_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_gold_ore : explosion_radius traits.put("deepslate_gold_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_iron_ore : explosion_radius traits.put("deepslate_iron_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_lapis_ore : explosion_radius traits.put("deepslate_lapis_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // deepslate_tile_slab : explosion_radius traits.put("deepslate_tile_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // diamond_ore : explosion_radius traits.put("diamond_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // diorite_slab : explosion_radius traits.put("diorite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // dispenser : block_entity traits.put("dispenser", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // dropper : block_entity traits.put("dropper", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // emerald_ore : explosion_radius traits.put("emerald_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // enchanting_table : block_entity traits.put("enchanting_table", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // end_stone_brick_slab : explosion_radius traits.put("end_stone_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // ender_chest : explosion_radius traits.put("ender_chest", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // exposed_cut_copper_slab : explosion_radius traits.put("exposed_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // flowering_azalea_leaves : tool traits.put("flowering_azalea_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // furnace : block_entity traits.put("furnace", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // gilded_blackstone : tool traits.put("gilded_blackstone", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_PICKAXE, 1) )); // gold_ore : explosion_radius traits.put("gold_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // granite_slab : explosion_radius traits.put("granite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // gravel : tool traits.put("gravel", Map.of( LootContext.TOOL, ItemStack.of(Material.IRON_SHOVEL, 1) )); // gray_banner : block_entity traits.put("gray_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // gray_candle : explosion_radius traits.put("gray_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // gray_shulker_box : block_entity traits.put("gray_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // green_banner : block_entity traits.put("green_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // green_candle : explosion_radius traits.put("green_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // green_shulker_box : block_entity traits.put("green_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // hopper : block_entity traits.put("hopper", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // iron_ore : explosion_radius traits.put("iron_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // jungle_leaves : tool traits.put("jungle_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // jungle_slab : explosion_radius traits.put("jungle_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // lapis_ore : explosion_radius traits.put("lapis_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // light_blue_banner : block_entity traits.put("light_blue_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // light_blue_candle : explosion_radius traits.put("light_blue_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // light_blue_shulker_box : block_entity traits.put("light_blue_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // light_gray_banner : block_entity traits.put("light_gray_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // light_gray_candle : explosion_radius traits.put("light_gray_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // light_gray_shulker_box : block_entity traits.put("light_gray_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // lime_banner : block_entity traits.put("lime_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // lime_candle : explosion_radius traits.put("lime_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // lime_shulker_box : block_entity traits.put("lime_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // magenta_banner : block_entity traits.put("magenta_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // magenta_candle : explosion_radius traits.put("magenta_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // magenta_shulker_box : block_entity traits.put("magenta_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // mangrove_leaves : tool traits.put("mangrove_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // mangrove_slab : explosion_radius traits.put("mangrove_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // mossy_cobblestone_slab : explosion_radius traits.put("mossy_cobblestone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // mossy_stone_brick_slab : explosion_radius traits.put("mossy_stone_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // mud_brick_slab : explosion_radius traits.put("mud_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // nether_brick_slab : explosion_radius traits.put("nether_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // nether_gold_ore : explosion_radius traits.put("nether_gold_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // nether_quartz_ore : explosion_radius traits.put("nether_quartz_ore", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // oak_leaves : tool traits.put("oak_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // oak_slab : explosion_radius traits.put("oak_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // orange_banner : block_entity traits.put("orange_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // orange_candle : explosion_radius traits.put("orange_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // orange_shulker_box : block_entity traits.put("orange_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // oxidized_cut_copper_slab : explosion_radius traits.put("oxidized_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // petrified_oak_slab : explosion_radius traits.put("petrified_oak_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // pink_banner : block_entity traits.put("pink_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // pink_candle : explosion_radius traits.put("pink_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // pink_petals : explosion_radius traits.put("pink_petals", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // pink_shulker_box : block_entity traits.put("pink_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // player_head : block_entity traits.put("player_head", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // polished_andesite_slab : explosion_radius traits.put("polished_andesite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // polished_blackstone_brick_slab : explosion_radius traits.put("polished_blackstone_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // polished_blackstone_slab : explosion_radius traits.put("polished_blackstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // polished_deepslate_slab : explosion_radius traits.put("polished_deepslate_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // polished_diorite_slab : explosion_radius traits.put("polished_diorite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // polished_granite_slab : explosion_radius traits.put("polished_granite_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // potatoes : explosion_radius traits.put("potatoes", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // prismarine_brick_slab : explosion_radius traits.put("prismarine_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // prismarine_slab : explosion_radius traits.put("prismarine_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // purple_banner : block_entity traits.put("purple_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // purple_candle : explosion_radius traits.put("purple_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // purple_shulker_box : block_entity traits.put("purple_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // purpur_slab : explosion_radius traits.put("purpur_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // quartz_slab : explosion_radius traits.put("quartz_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // red_banner : block_entity traits.put("red_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // red_candle : explosion_radius traits.put("red_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // red_nether_brick_slab : explosion_radius traits.put("red_nether_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // red_sandstone_slab : explosion_radius traits.put("red_sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // red_shulker_box : block_entity traits.put("red_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // sandstone_slab : explosion_radius traits.put("sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // sea_pickle : explosion_radius traits.put("sea_pickle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // shulker_box : block_entity traits.put("shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // smoker : block_entity traits.put("smoker", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // smooth_quartz_slab : explosion_radius traits.put("smooth_quartz_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // smooth_red_sandstone_slab : explosion_radius traits.put("smooth_red_sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // smooth_sandstone_slab : explosion_radius traits.put("smooth_sandstone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // smooth_stone_slab : explosion_radius traits.put("smooth_stone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // snow_block : explosion_radius traits.put("snow_block", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // spruce_leaves : tool traits.put("spruce_leaves", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // spruce_slab : explosion_radius traits.put("spruce_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // stone_brick_slab : explosion_radius traits.put("stone_brick_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // stone_slab : explosion_radius traits.put("stone_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // torchflower_crop : explosion_radius traits.put("torchflower_crop", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // trapped_chest : block_entity traits.put("trapped_chest", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // twisting_vines : tool traits.put("twisting_vines", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // twisting_vines_plant : tool traits.put("twisting_vines_plant", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // warped_slab : explosion_radius traits.put("warped_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // waxed_cut_copper_slab : explosion_radius traits.put("waxed_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // waxed_exposed_cut_copper_slab : explosion_radius traits.put("waxed_exposed_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // waxed_oxidized_cut_copper_slab : explosion_radius traits.put("waxed_oxidized_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // waxed_weathered_cut_copper_slab : explosion_radius traits.put("waxed_weathered_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // weathered_cut_copper_slab : explosion_radius traits.put("weathered_cut_copper_slab", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // weeping_vines : tool traits.put("weeping_vines", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // weeping_vines_plant : tool traits.put("weeping_vines_plant", Map.of( LootContext.TOOL, ItemStack.of(Material.DIAMOND_AXE, 1) )); // wheat : explosion_radius traits.put("wheat", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // white_banner : block_entity traits.put("white_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // white_candle : explosion_radius traits.put("white_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // white_shulker_box : block_entity traits.put("white_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // yellow_banner : block_entity traits.put("yellow_banner", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); // yellow_candle : explosion_radius traits.put("yellow_candle", Map.of( LootContext.EXPLOSION_RADIUS, 0.0 )); // yellow_shulker_box : block_entity traits.put("yellow_shulker_box", Map.of( LootContext.BLOCK_ENTITY, CompoundBinaryTag.empty() )); TRAITS = Map.copyOf(traits); } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/loot/LootTableTests.java ================================================ package net.minestom.vanilla.datapack.loot; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.serialization.JsonOps; import io.github.pesto.MojangDataFeature; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.TagStringIO; import net.minecraft.SharedConstants; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.nbt.TagParser; import net.minecraft.server.Bootstrap; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.context.ContextKeySet; import net.minecraft.util.context.ContextMap; import net.minecraft.world.level.storage.loot.LootParams; import net.minestom.server.MinecraftServer; import net.minestom.server.item.ItemStack; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.blocks.VanillaBlockLoot; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.DatapackLoader; import net.minestom.vanilla.datapack.DatapackLoadingFeature; import net.minestom.vanilla.datapack.loot.context.LootContext; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; public class LootTableTests { private static VanillaReimplementation vri; private static Datapack datapack; private static FileSystem loot_tables; private static FileSystem loot_table_files; @BeforeAll public static void init() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); MinecraftServer.init(); vri = VanillaReimplementation.hook(MinecraftServer.process()); FileSystem data = vri.feature(MojangDataFeature.class).latestAssets(); FileSystem fs = data.map(byteArray -> byteArray.toCharacterString()); fs = fs.folder("minecraft", "loot_tables"); DatapackLoadingFeature feature = vri.feature(DatapackLoadingFeature.class); datapack = feature.current(); loot_tables = datapack.namespacedData().get("minecraft").loot_tables(); loot_table_files = fs; } // @Test // public void testMinestomBlocks() { // FileSystem blocks = loot_table_files.folder("blocks"); // // for (String file : blocks.files().stream().sorted().toList()) { // String src = blocks.file(file); // Table table = table(src); // String filename = file.substring(0, file.length() - ".json".length()); // List expected = LootTableTestData.EXPECTED_RESULTS.get(filename); // // List minestom; // try { // Map, Object> traits = LootTableTestData.TRAITS.getOrDefault(filename, Map.of()); // minestom = table.minestomLoot(traits); // } catch (Exception e) { // if (e instanceof IllegalStateException && e.getMessage().contains("LootContext does not have trait ")) { // String trait = e.getMessage().substring("LootContext does not have trait ".length()); // System.out.println("Skipping test for " + filename + " because trait " + trait + " is missing"); // continue; // } // throw new RuntimeException("Failed to run LootTable: " + file, e); // } // // if (expected == null) { // String testDataValue = "List.of(" + minestom.stream() // .map(Item::minestom) // .map(stack -> "ItemStack.of(Material." + stack.material().key().value().toUpperCase(Locale.ROOT) + ", " + stack.amount() + ")") // .collect(Collectors.joining(", ")) + ")"; // String testDataLine = "expected.put(\"" + filename + "\", " + testDataValue + ");"; // System.out.println(testDataLine); // continue; // } // assertItems(filename, minestom, expected); // } // } private void assertItems(String label, List item, ItemStack... expected) { assertItems(label, item, List.of(expected)); } private void assertItems(String label, List item, List expected) { List items = item.stream().map(Item::minestom).toList(); assertEquals(expected.size(), items.size(), () -> label + " size mismatch"); for (int i = 0; i < items.size(); i++) { ItemStack actual = items.get(i); ItemStack expectedItem = expected.get(i); int finalI = i; assertEquals(expectedItem.material(), actual.material(), () -> label + " material mismatch at index " + finalI); assertEquals(expectedItem.amount(), actual.amount(), () -> label + " amount mismatch at index " + finalI); // TODO: nbt comparison // this is non-trivial as '{0=>CustomData[nbt=BinaryTagType[CompoundBinaryTag 10]{tags={"tag"=BinaryTagType[CompoundBinaryTag 10]{tags={}}}}]}' must equal '{}' (empty compound tag) // assertEquals(expectedItem.nbt(), actual.nbt()); } } record Table( net.minecraft.world.level.storage.loot.LootTable vanilla, LootTable minestom ) { @SuppressWarnings("DataFlowIssue") public List vanillaLoot() { // we somehow need to acquire a "ServerLevel" instance. Hopefully we don't need to actually run a vanilla server. // check out net.minecraft.server.Main if we need to run a server ServerLevel level = new ServerLevel(null, null, null, null, null, null, null, false, 0, List.of(), false, null); LootParams params = new LootParams(level, new ContextMap.Builder().create(new ContextKeySet.Builder().build()), Map.of(), 0.0f); var items = vanilla.getRandomItems(params); return items.stream().map(LootTableTests::item).toList(); } public List minestomLoot(Map, Object> traits) { LootContext context = new LootContext() { @Override public @Nullable T get(Trait trait) { //noinspection unchecked return (T) traits.get(trait); } }; VanillaBlockLoot loot = new VanillaBlockLoot(vri, datapack); var items = loot.getLoot(minestom, context); return items.stream().map(LootTableTests::item).toList(); } } private Table table(String src) { Gson gson = new Gson(); JsonElement json = gson.fromJson(src, JsonElement.class); net.minecraft.world.level.storage.loot.LootTable vanilla = net.minecraft.world.level.storage.loot.LootTable.CODEC.parse(JsonOps.INSTANCE, json).result().orElseThrow().value(); LootTable minestom = DatapackLoader.adaptor(LootTable.class).apply(src); return new Table(vanilla, minestom); } interface Item { net.minecraft.world.item.ItemStack vanilla(); net.minestom.server.item.ItemStack minestom(); } private static Item item(ItemStack minestom) { try { String snbt = TagStringIO.get().asString(minestom.toItemNBT()); return new SNBTItem(snbt); } catch (IOException e) { throw new RuntimeException(e); } } private static Item item(net.minecraft.world.item.ItemStack vanilla) { Tag tag = vanilla.save(HolderLookup.Provider.create(Stream.empty()), new CompoundTag()); return new SNBTItem(tag.toString()); } } record SNBTItem(String item) implements LootTableTests.Item { @Override public net.minecraft.world.item.ItemStack vanilla() { try { CompoundTag tag = TagParser.parseCompoundFully(item); return net.minecraft.world.item.ItemStack.parse(HolderLookup.Provider.create(Stream.empty()), tag).get(); } catch (CommandSyntaxException e) { throw new RuntimeException(e); } } @Override public net.minestom.server.item.ItemStack minestom() { try { CompoundBinaryTag tag = TagStringIO.get().asCompound(item); return ItemStack.fromItemNBT(tag); } catch (IOException e) { throw new RuntimeException(e); } } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/worldgen/DF.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.mojang.serialization.JsonOps; import net.minestom.vanilla.datapack.DatapackLoader; interface DF { double compute(double x, double y, double z); record VanillaDF(net.minecraft.world.level.levelgen.DensityFunction df) implements DF { @Override public double compute(double x, double y, double z) { return df.compute(createFunctionContext((int) x, (int) y, (int) z)); } } static DF vanilla(String source) { JsonElement element = new Gson().fromJson(source, JsonElement.class); var result = net.minecraft.world.level.levelgen.DensityFunction.HOLDER_HELPER_CODEC.parse(JsonOps.INSTANCE, element); var df = result.getOrThrow(error -> { throw new RuntimeException(error); }); return new VanillaDF(df); } record VriDF(DensityFunction df) implements DF { @Override public double compute(double x, double y, double z) { return df.compute(DensityFunction.context(x, y, z)); } } static DF vri(String source) { DensityFunction df = DatapackLoader.adaptor(DensityFunction.class).apply(source); return new VriDF(df); } private static net.minecraft.world.level.levelgen.DensityFunction.FunctionContext createFunctionContext(int x, int y, int z) { return new net.minecraft.world.level.levelgen.DensityFunction.FunctionContext() { @Override public int blockX() { return x; } @Override public int blockY() { return y; } @Override public int blockZ() { return z; } }; } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/worldgen/DFVisualizer.java ================================================ package net.minestom.vanilla.datapack.worldgen; import com.google.common.util.concurrent.AtomicDouble; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; public class DFVisualizer { public enum Axis { X, Y, Z } static void visualize2d(String source, double scale, Axis axisToIgnore) { visualize2d(DF.vanilla(source), DF.vri(source), scale, axisToIgnore); } static void visualize2d(DF vanilla, DF vri, double scale, Axis axisToSpecify) { Dimension dimension = new Dimension(512, 512); BufferedImage vanillaImage = generateImage(vanilla, scale, dimension, axisToSpecify, 0); BufferedImage vriImage = generateImage(vri, scale, dimension, axisToSpecify, 0); JLabel vanillaLabel = new JLabel(new ImageIcon(vanillaImage)); JLabel vriLabel = new JLabel(new ImageIcon(vriImage)); // create window, add images, and block this thread until the window is closed JFrame frame = new JFrame("Density Function Visualizer"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().setLayout(new FlowLayout()); frame.getContentPane().add(vanillaLabel); frame.getContentPane().add(vriLabel); // Add a slider for the axis value JSlider slider = new JSlider(JSlider.HORIZONTAL, -100, 100, 0); slider.setMajorTickSpacing(20); slider.setPaintTicks(true); slider.setPaintLabels(true); // register a listener to update the image when the slider is moved AtomicDouble axisValue = new AtomicDouble(0); AtomicDouble target = new AtomicDouble(0); slider.addChangeListener(e -> target.set(slider.getValue())); Thread daemon = new Thread(() -> { while (true) { System.out.println("target: " + target.get() + ", axisValue: " + axisValue.get()); if (target.get() == axisValue.get()) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } continue; } double currentTarget = target.get(); System.out.println("Updating image"); BufferedImage vanillaImage1 = generateImage(vanilla, scale, dimension, axisToSpecify, currentTarget); BufferedImage vriImage1 = generateImage(vri, scale, dimension, axisToSpecify, currentTarget); vanillaLabel.setIcon(new ImageIcon(vanillaImage1)); vriLabel.setIcon(new ImageIcon(vriImage1)); frame.pack(); frame.repaint(); axisValue.set(currentTarget); } }); daemon.setDaemon(true); daemon.start(); frame.getContentPane().add(slider); frame.pack(); frame.setVisible(true); try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } } private static BufferedImage generateImage(DF df, double scale, Dimension imageDimensions, Axis axisToSpecify, double axisValue) { BufferedImage image = new BufferedImage(imageDimensions.width, imageDimensions.height, BufferedImage.TYPE_INT_RGB); Graphics2D graphics = image.createGraphics(); graphics.setColor(Color.BLACK); graphics.fillRect(0, 0, imageDimensions.width, imageDimensions.height); double xStep = scale / imageDimensions.width; double zStep = scale / imageDimensions.height; for (int x = 0; x < imageDimensions.width; x++) { for (int z = 0; z < imageDimensions.height; z++) { double xCoord = ((double) x - (double) imageDimensions.width / 2.0) * xStep; double zCoord = ((double) z - (double) imageDimensions.height / 2.0) * zStep; double yCoord = switch (axisToSpecify) { case X -> df.compute(axisValue, xCoord, zCoord); case Y -> df.compute(xCoord, axisValue, zCoord); case Z -> df.compute(xCoord, zCoord, axisValue); }; double alpha = Math.min(1.0, Math.max(0.0, yCoord)); graphics.setColor(new Color(1.0f, 1.0f, 1.0f, (float) alpha)); graphics.fillRect(x, z, 1, 1); } } image.flush(); return image; } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/worldgen/DensityFunctionTests.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.minecraft.SharedConstants; import net.minecraft.server.Bootstrap; import net.minestom.server.coordinate.Vec; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.Random; import java.util.function.BiConsumer; import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; public class DensityFunctionTests { private static final double DELTA = 0.0001; @BeforeAll public static void prepare() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } // End islands are used in many other tests. So test them first. private final String END_ISLANDS = "{ \"type\": \"minecraft:end_islands\" }"; @Test public void testEndIslands() { assertExact(END_ISLANDS); } @Test public void testConstant() { assertExact("1.0"); assertExact("0.0"); assertExact("-1.0"); assertExact("0.5"); assertExact("1"); assertExact("0"); } @Test public void testClamp() { assertExact(String.format(""" { "type": "minecraft:clamp", "input": %s, "min": -0.5, "max": 0.5 } """, END_ISLANDS)); assertExact(String.format(""" { "type": "minecraft:clamp", "input": %s, "min": -0.23, "max": 1.0 } """, END_ISLANDS)); assertExact(String.format(""" { "type": "minecraft:clamp", "input": %s, "min": -1.0, "max": 0.23 } """, END_ISLANDS)); assertExact(String.format(""" { "type": "minecraft:clamp", "input": %s, "min": -0.23, "max": 0.23 } """, END_ISLANDS)); assertExact(String.format(""" { "type": "minecraft:clamp", "input": %s, "min": 0.23, "max": 0.23 } """, END_ISLANDS)); } @Test public void testAbs() { assertExact(String.format(""" { "type": "minecraft:abs", "argument": %s } """, END_ISLANDS)); } @Test public void testSquare() { assertExact(String.format(""" { "type": "minecraft:square", "argument": %s } """, END_ISLANDS)); } private final String END_ISLANDS_CUBED = String.format(""" { "type": "minecraft:cube", "argument": %s } """, END_ISLANDS); @Test public void testCube() { assertExact(END_ISLANDS_CUBED); } @Test public void testHalfNegative() { assertExact(String.format(""" { "type": "minecraft:half_negative", "argument": %s } """, END_ISLANDS)); } @Test public void testQuarterNegative() { assertExact(String.format(""" { "type": "minecraft:quarter_negative", "argument": %s } """, END_ISLANDS)); } @Test public void testSqueeze() { assertExact(String.format(""" { "type": "minecraft:squeeze", "argument": %s } """, END_ISLANDS)); } @Test public void testAdd() { assertExact(String.format(""" { "type": "minecraft:add", "argument1": %s, "argument2": %s } """, END_ISLANDS, END_ISLANDS_CUBED)); } @Test public void testMul() { assertExact(String.format(""" { "type": "minecraft:mul", "argument1": %s, "argument2": %s } """, END_ISLANDS, END_ISLANDS_CUBED)); assertExact(String.format(""" { "type": "minecraft:mul", "argument1": %s, "argument2": 2.0 } """, END_ISLANDS)); assertExact(String.format(""" { "type": "minecraft:mul", "argument1": %s, "argument2": 128 } """, END_ISLANDS)); } @Test public void testMin() { assertExact(String.format(""" { "type": "minecraft:min", "argument1": %s, "argument2": %s } """, END_ISLANDS, END_ISLANDS_CUBED)); } @Test public void testMax() { assertExact(String.format(""" { "type": "minecraft:max", "argument1": %s, "argument2": %s } """, END_ISLANDS, END_ISLANDS_CUBED)); } // Noise is the big boi, so test it thoroughly. @Test public void testNoise() { assertExact(""" { "type": "minecraft:noise", "noise": { "firstOctave": -3, "amplitudes": [ 1 ] }, "xz_scale": 1.0, "y_scale": 1.0 } """); } private void testPositions(BiConsumer consumer) { double dist = 0.0001; Random random = new Random(0); int i = 0; while (dist < 100000.0) { dist *= 1.1; double x = random.nextDouble(-dist, dist); double y = random.nextDouble(-dist, dist); double z = random.nextDouble(-dist, dist); consumer.accept(new Vec(x, y, z), i++); } } private void assertExact(String source) { assertExact(DF.vanilla(source), DF.vri(source), DELTA); } private void assertExact(DF vanilla, DF vri, double delta) { testPositions((pos, i) -> { var result = compare(vanilla, vri, pos.blockX(), pos.blockY(), pos.blockZ()); int finalI = i; result.assertEqual(delta, () -> "Failed at " + pos + " (index " + finalI + ")"); }); } private record Result(double vanilla, double vri) { public void assertEqual(double delta, Supplier message) { assertEquals(vanilla, vri, delta, message); } } private Result compare(DF vanilla, DF vri, int x, int y, int z) { var vanillaRes = vanilla.compute(x, y, z); var vriRes = vri.compute(x, y, z); return new Result(vanillaRes, vriRes); } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/worldgen/NoiseTests.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minestom.vanilla.datapack.worldgen.noise.SimplexNoise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import org.junit.jupiter.api.Test; import java.util.Random; import static org.junit.jupiter.api.Assertions.assertEquals; public class NoiseTests { @Test public void testSimplex() { testSimplex(0); testSimplex(123); testSimplex(123456789); testSimplex(-123456789); } private void testSimplex(long seed) { Noise vanilla = simplexVanilla(seed); Noise vri = simplexVri(seed); Random random = new Random(seed); // 3d for (int i = 0; i < 1000; i++) { double bound = Math.sqrt(i + 1); double x = random.nextDouble(-bound, bound); double y = random.nextDouble(-bound, bound); double z = random.nextDouble(-bound, bound); double vanillaRes = vanilla.sample(x, y, z); double vriRes = vri.sample(x, y, z); assertEquals(vanillaRes, vriRes, 0.000001, "x=" + x + ", y=" + y + ", z=" + z + " failed (i=" + i + ")"); } // 2d for (int i = 0; i < 1000; i++) { double bound = Math.sqrt(i + 1); double x = random.nextDouble(-bound, bound); double y = random.nextDouble(-bound, bound); double vanillaRes = vanilla.sample2d(x, y); double vriRes = vri.sample2d(x, y); assertEquals(vanillaRes, vriRes, 0.000001, "x=" + x + ", y=" + y + " failed (i=" + i + ")"); } } private interface Noise { double sample(double x, double y, double z); double sample2d(double x, double y); } private static Noise simplexVanilla(long seed) { var noise = new net.minecraft.world.level.levelgen.synth.SimplexNoise(new LegacyRandomSource(seed)); return new Noise() { @Override public double sample(double x, double y, double z) { return noise.getValue(x, y, z); } @Override public double sample2d(double x, double y) { return noise.getValue(x, y); } }; } private static Noise simplexVri(long seed) { var noise = new SimplexNoise(WorldgenRandom.legacy(seed)); return new Noise() { @Override public double sample(double x, double y, double z) { return noise.sample(x, y, z); } @Override public double sample2d(double x, double y) { return noise.sample2D(x, y); } }; } } ================================================ FILE: datapack-tests/src/test/java/net/minestom/vanilla/datapack/worldgen/RandomTests.java ================================================ package net.minestom.vanilla.datapack.worldgen; import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.XoroshiroRandomSource; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class RandomTests { @Test public void testLegacyRandom() { testLegacyRandom(0); testLegacyRandom(534); testLegacyRandom(-49273); testLegacyRandom(123456789); } private void testLegacyRandom(long seed) { var vri = WorldgenRandom.legacy(seed); var vanilla = new LegacyRandomSource(seed); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextInt(), vri.nextInt(), "Iteration " + i); assertEquals(vanilla.nextInt(i + 8), vri.nextInt(i + 8), "Iteration " + i); } vri.consumeInt(19283); vanilla.consumeCount(19283); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextLong(), vri.nextLong(), "Iteration " + i); } vri.consumeInt(19283); vanilla.consumeCount(19283); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextDouble(), vri.nextDouble(), "Iteration " + i); } } @Test public void testXoroshiroRandom() { var vri = WorldgenRandom.xoroshiro(0); var vanilla = new XoroshiroRandomSource(0); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextInt(), vri.nextInt(), "Iteration " + i); assertEquals(vanilla.nextInt(i + 8), vri.nextInt(i + 8), "Iteration " + i); } vri.consumeLong(19283); vanilla.consumeCount(19283); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextLong(), vri.nextLong(), "Iteration " + i); } vri.consumeInt(19283); vanilla.consumeCount(19283); for (int i = 0; i < 100; i++) { assertEquals(vanilla.nextDouble(), vri.nextDouble(), "Iteration " + i); } } } ================================================ FILE: entities/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":entity-meta")) } ================================================ FILE: entities/src/main/java/net/minestom/vanilla/entities/FallingBlockEntity.java ================================================ package net.minestom.vanilla.entities; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.metadata.other.FallingBlockMeta; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.VanillaRegistry; import net.minestom.vanilla.entitymeta.EntityTags; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Random; public class FallingBlockEntity extends Entity { private static final Random rng = new Random(); private final @NotNull Block toPlace; public FallingBlockEntity(@NotNull Block toPlace, @NotNull Pos initialPosition) { super(EntityType.FALLING_BLOCK); this.toPlace = toPlace; // setGravity(0.025f, getGravityAcceleration()); setBoundingBox(0.98f, 0.98f, 0.98f); FallingBlockMeta meta = (FallingBlockMeta) this.getEntityMeta(); meta.setBlock(toPlace); meta.setSpawnPosition(initialPosition); } public FallingBlockEntity(@NotNull VanillaRegistry.EntityContext context) { this(Objects.requireNonNullElse(context.getTag(EntityTags.FallingBlock.BLOCK), Block.AIR), context.position()); } @Override public void update(long time) { // TODO: Cleanup this method structure // TODO: This isOnGround method seems to snap the entity to the ground earlier than expected if (!isOnGround()) { return; } Block block = instance.getBlock(position); if (block.registry().isSolid()) { // TODO: Better way to get block's loot Material loot = block.registry().material(); if (loot != null) { ItemEntity itemEntity = new ItemEntity(ItemStack.of(loot)); itemEntity.setInstance(instance, position); } remove(); return; } instance.setBlock(position, toPlace); remove(); } } ================================================ FILE: entities/src/main/java/net/minestom/vanilla/entities/MinestomEntitiesFeature.java ================================================ package net.minestom.vanilla.entities; import net.kyori.adventure.key.Key; import net.minestom.server.entity.EntityType; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; public class MinestomEntitiesFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { context.registry().register(EntityType.FALLING_BLOCK, FallingBlockEntity::new); context.registry().register(EntityType.TNT, PrimedTNTEntity::new); } @Override public @NotNull Key key() { return Key.key("vri:entities"); } } ================================================ FILE: entities/src/main/java/net/minestom/vanilla/entities/PrimedTNTEntity.java ================================================ package net.minestom.vanilla.entities; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.metadata.other.PrimedTntMeta; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaRegistry; import net.minestom.vanilla.entitymeta.EntityTags; import net.minestom.vanilla.instance.VanillaExplosion; import org.jetbrains.annotations.NotNull; import java.util.Objects; public class PrimedTNTEntity extends Entity { private int fuseTime; public PrimedTNTEntity(@NotNull VanillaRegistry.EntityContext context) { this(Objects.requireNonNullElse(context.getTag(EntityTags.PrimedTnt.FUSE_TIME), 80)); } public PrimedTNTEntity(int fuseTime) { super(EntityType.TNT); setAerodynamics(getAerodynamics().withVerticalAirResistance(0.98f)); setBoundingBox(0.98f, 0.98f, 0.98f); this.fuseTime = fuseTime; PrimedTntMeta meta = (PrimedTntMeta) this.getEntityMeta(); meta.setFuseTime(fuseTime); } private void explode() { Instance instance = this.instance; remove(); Block block = instance.getBlock(this.getPosition()); VanillaExplosion explosion = VanillaExplosion.builder(getPosition(), 4.0f) .destroyBlocks(!block.isLiquid()) .build(); explosion.trigger(instance); } @Override public void update(long time) { super.update(time); if (fuseTime-- <= 0) { explode(); } } public int getFuseTime() { return fuseTime; } public void setFuseTime(int fuseTime) { this.fuseTime = fuseTime; } } ================================================ FILE: entity-meta/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: entity-meta/src/main/java/net/minestom/vanilla/entitymeta/EntityTags.java ================================================ package net.minestom.vanilla.entitymeta; import net.minestom.server.instance.block.Block; import net.minestom.server.tag.Tag; import org.jetbrains.annotations.NotNull; public interface EntityTags { interface FallingBlock { @NotNull Tag BLOCK = Tag.String("vri:entity-meta.falling_block.block") .map(Block::fromKey, block -> block.key().toString()); } interface PrimedTnt { @NotNull Tag FUSE_TIME = Tag.Integer("vri:entity-meta.primed_tnt.fuse_time"); } } ================================================ FILE: fluid-simulation/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":block-update-system")) } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/EmptyFluid.java ================================================ package io.github.togar2.fluids; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.server.utils.Direction; public class EmptyFluid extends Fluid { public EmptyFluid() { super(Block.AIR, Material.BUCKET); } @Override protected boolean canBeReplacedWith(Instance instance, Point point, Fluid other, Direction direction) { return true; } @Override public int getNextTickDelay(Instance instance, Point point, Block block) { return -1; } @Override protected boolean isEmpty() { return true; } @Override protected double getBlastResistance() { return 0; } @Override public double getHeight(Block block, Instance instance, Point point) { return 0; } @Override public double getHeight(Block block) { return 0; } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/FlowableFluid.java ================================================ package io.github.togar2.fluids; import it.unimi.dsi.fastutil.shorts.Short2BooleanMap; import it.unimi.dsi.fastutil.shorts.Short2BooleanOpenHashMap; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.gamedata.tags.Tag; import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.server.utils.Direction; import java.util.EnumMap; import java.util.Map; import java.util.Objects; public abstract class FlowableFluid extends Fluid { public FlowableFluid(Block defaultBlock, Material bucket) { super(defaultBlock, bucket); } @Override public void onTick(Instance instance, Point point, Block block) { if (!isSource(block)) { Block updated = getUpdatedState(instance, point, block); if (MinestomFluids.get(updated).isEmpty()) { block = updated; instance.setBlock(point, Block.AIR); } else if (updated != block) { block = updated; instance.setBlock(point, updated); } } tryFlow(instance, point, block); } @Override public int getNextTickDelay(Instance instance, Point point, Block block) { return getTickRate(instance); } protected void tryFlow(Instance instance, Point point, Block block) { Fluid fluid = MinestomFluids.get(block); if (fluid.isEmpty()) return; Point down = point.add(0, -1, 0); Block downBlock = instance.getBlock(down); Block updatedDownFluid = getUpdatedState(instance, down, downBlock); if (canFlow(instance, point, block, Direction.DOWN, down, downBlock, updatedDownFluid)) { flow(instance, down, downBlock, Direction.DOWN, updatedDownFluid); if (getAdjacentSourceCount(instance, point) >= 3) { flowSides(instance, point, block); } } else if (isSource(block) || !canFlowDown(instance, updatedDownFluid, point, block, down, downBlock)) { flowSides(instance, point, block); } } /** * Flows to the sides whenever possible, or to a hole if found */ private void flowSides(Instance instance, Point point, Block block) { int newLevel = getLevel(block) - getLevelDecreasePerBlock(instance); if (isFalling(block)) newLevel = 7; if (newLevel <= 0) return; Map map = getSpread(instance, point, block); for (Map.Entry entry : map.entrySet()) { Direction direction = entry.getKey(); Block newBlock = entry.getValue(); Point offset = point.add(direction.normalX(), direction.normalY(), direction.normalZ()); Block currentBlock = instance.getBlock(offset); if (!canFlow(instance, point, block, direction, offset, currentBlock, newBlock)) continue; flow(instance, offset, currentBlock, direction, newBlock); } } /** * Gets the updated state of a source block by taking into account its surrounding blocks. */ protected Block getUpdatedState(Instance instance, Point point, Block block) { int highestLevel = 0; int stillCount = 0; for (Direction direction : Direction.HORIZONTAL) { Point directionPos = point.add(direction.normalX(), direction.normalY(), direction.normalZ()); Block directionBlock = instance.getBlock(directionPos); Fluid directionFluid = MinestomFluids.get(directionBlock); if (directionFluid != this || !receivesFlow(direction, instance, point, block, directionPos, directionBlock)) continue; if (isSource(directionBlock)) { ++stillCount; } highestLevel = Math.max(highestLevel, getLevel(directionBlock)); } if (isInfinite() && stillCount >= 2) { // If there's 2 or more still fluid blocks around // and below is still or a solid block, make this block still Block downBlock = instance.getBlock(point.add(0, -1, 0)); if (downBlock.isSolid() || isMatchingAndStill(downBlock)) { return getSource(false); } } Point above = point.add(0, 1, 0); Block aboveBlock = instance.getBlock(above); Fluid aboveFluid = MinestomFluids.get(aboveBlock); if (!aboveFluid.isEmpty() && aboveFluid == this && receivesFlow(Direction.UP, instance, point, block, above, aboveBlock)) { return getFlowing(8, true); } int newLevel = highestLevel - getLevelDecreasePerBlock(instance); if (newLevel <= 0) return Block.AIR; return getFlowing(newLevel, false); } private boolean receivesFlow(Direction face, Instance instance, Point point, Block block, Point fromPoint, Block fromBlock) { // Vanilla seems to check if the adjacent block shapes cover the same square, but this seems to work as well // (Might not work with some special blocks) // If there is anything wrong it is most likely this method :D if (block.isLiquid()) { if (face == Direction.UP) { if (fromBlock.isLiquid()) return true; return block.isSolid() || block.isAir(); //return isSource(block) || getLevel(block) == 8; } else if (face == Direction.DOWN) { if (fromBlock.isLiquid()) return true; return fromBlock.isSolid() || fromBlock.isAir(); //return isSource(fromBlock) || getLevel(fromBlock) == 8; } else { return true; } } else { if (face == Direction.UP) { return block.isSolid() || block.isAir(); } else if (face == Direction.DOWN) { return block.isSolid() || block.isAir(); } else { return block.isSolid() || block.isAir(); } } } /** * Creates a unique id based on the relation between point and point2 */ private static short getID(Point point, Point point2) { int i = (int) (point2.x() - point.x()); int j = (int) (point2.z() - point.z()); return (short) ((i + 128 & 0xFF) << 8 | j + 128 & 0xFF); } /** * Returns a map with the directions the water can flow in and the block the water will become in that direction. * If a hole is found within {@code getHoleRadius()} blocks, the water will only flow in that direction. * A weight is used to determine which hole is the closest. */ protected Map getSpread(Instance instance, Point point, Block block) { int weight = 1000; EnumMap map = new EnumMap<>(Direction.class); Short2BooleanOpenHashMap holeMap = new Short2BooleanOpenHashMap(); for (Direction direction : Direction.HORIZONTAL) { Point directionPoint = point.add(direction.normalX(), direction.normalY(), direction.normalZ()); Block directionBlock = instance.getBlock(directionPoint); short id = FlowableFluid.getID(point, directionPoint); Block updatedBlock = getUpdatedState(instance, directionPoint, directionBlock); if (!canFlowThrough(instance, updatedBlock, point, block, direction, directionPoint, directionBlock)) continue; boolean down = holeMap.computeIfAbsent(id, s -> { Point downPoint = directionPoint.add(0, -1, 0); return canFlowDown( instance, getFlowing(getLevel(updatedBlock), false), directionPoint, directionBlock, downPoint, instance.getBlock(downPoint) ); }); int newWeight = down ? 0 : getWeight(instance, directionPoint, 1, direction.opposite(), directionBlock, point, holeMap); if (newWeight < weight) map.clear(); if (newWeight <= weight) { map.put(direction, updatedBlock); weight = newWeight; } } return map; } protected int getWeight(Instance instance, Point point, int initialWeight, Direction skipCheck, Block block, Point originalPoint, Short2BooleanMap short2BooleanMap) { int weight = 1000; for (Direction direction : Direction.HORIZONTAL) { if (direction == skipCheck) continue; Point directionPoint = point.add(direction.normalX(), direction.normalY(), direction.normalZ()); Block directionBlock = instance.getBlock(directionPoint); short id = FlowableFluid.getID(originalPoint, directionPoint); if (!canFlowThrough(instance, getFlowing(getLevel(block), false), point, block, direction, directionPoint, directionBlock)) continue; boolean down = short2BooleanMap.computeIfAbsent(id, s -> { Point downPoint = directionPoint.add(0, -1, 0); Block downBlock = instance.getBlock(downPoint); return canFlowDown( instance, getFlowing(getLevel(block), false), directionPoint, downBlock, downPoint, downBlock ); }); if (down) return initialWeight; if (initialWeight < getHoleRadius(instance)) { int newWeight = getWeight(instance, directionPoint, initialWeight + 1, direction.opposite(), directionBlock, originalPoint, short2BooleanMap); if (newWeight < weight) weight = newWeight; } } return weight; } private int getAdjacentSourceCount(Instance instance, Point point) { int i = 0; for (Direction direction : Direction.HORIZONTAL) { Point currentPoint = point.add(direction.normalX(), direction.normalY(), direction.normalZ()); Block block = instance.getBlock(currentPoint); if (!isMatchingAndStill(block)) continue; ++i; } return i; } /** * Returns whether the fluid can flow through a specific block */ private boolean canFill(Instance instance, Point point, Block block, Block flowing) { //TODO check waterloggable TagManager tags = MinecraftServer.getTagManager(); if (block.compare(Block.LADDER) || block.compare(Block.SUGAR_CANE) || block.compare(Block.BUBBLE_COLUMN) || block.compare(Block.NETHER_PORTAL) || block.compare(Block.END_PORTAL) || block.compare(Block.END_GATEWAY) || block.compare(Block.KELP) || block.compare(Block.KELP_PLANT) || block.compare(Block.SEAGRASS) || block.compare(Block.TALL_SEAGRASS) || block.compare(Block.SEA_PICKLE) || Objects.requireNonNull(tags.getTag(Tag.BasicType.BLOCKS, "minecraft:signs")).contains(block.key()) || block.name().contains("door") || block.name().contains("coral")) { return false; } return !block.isSolid(); } private boolean canFlowDown(Instance instance, Block flowing, Point point, Block block, Point fromPoint, Block fromBlock) { if (!this.receivesFlow(Direction.DOWN, instance, point, block, fromPoint, fromBlock)) return false; if (MinestomFluids.get(fromBlock) == this) return true; return this.canFill(instance, fromPoint, fromBlock, flowing); } private boolean canFlowThrough(Instance instance, Block flowing, Point point, Block block, Direction face, Point fromPoint, Block fromBlock) { return !isMatchingAndStill(fromBlock) && receivesFlow(face, instance, point, block, fromPoint, fromBlock) && canFill(instance, fromPoint, fromBlock, flowing); } protected boolean canFlow(Instance instance, Point fluidPoint, Block flowingBlock, Direction flowDirection, Point flowTo, Block flowToBlock, Block newFlowing) { return MinestomFluids.get(flowToBlock).canBeReplacedWith(instance, flowTo, MinestomFluids.get(newFlowing), flowDirection) && receivesFlow(flowDirection, instance, fluidPoint, flowingBlock, flowTo, flowToBlock) && canFill(instance, flowTo, flowToBlock, newFlowing); } /** * Sets the position to the new block, executing {@code onBreakingBlock()} before breaking any non-air block. */ protected void flow(Instance instance, Point point, Block block, Direction direction, Block newBlock) { //TODO waterloggable check boolean cancel = false; if (!block.isAir()) { if (!onBreakingBlock(instance, point, block)) cancel = true; } if (!cancel) instance.setBlock(point, newBlock); } private boolean isMatchingAndStill(Block block) { return MinestomFluids.get(block) == this && isSource(block); } public Block getFlowing(int level, boolean falling) { return defaultBlock.withProperty("level", String.valueOf(falling ? 8 : level)); } public Block getSource(boolean falling) { return falling ? defaultBlock.withProperty("level", "8") : defaultBlock; } protected abstract boolean isInfinite(); protected abstract int getLevelDecreasePerBlock(Instance instance); protected abstract int getHoleRadius(Instance instance); /** * Returns whether the block can be broken */ protected abstract boolean onBreakingBlock(Instance instance, Point point, Block block); public abstract int getTickRate(Instance instance); private static boolean isFluidAboveEqual(Block block, Instance instance, Point point) { return MinestomFluids.get(block) == MinestomFluids.get(instance.getBlock(point.add(0, 1, 0))); } @Override public double getHeight(Block block, Instance instance, Point point) { return isFluidAboveEqual(block, instance, point) ? 1 : getHeight(block); } @Override public double getHeight(Block block) { return getLevel(block) / 9.0; } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/Fluid.java ================================================ package io.github.togar2.fluids; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.utils.Direction; public abstract class Fluid { protected final Block defaultBlock; private final ItemStack bucket; public Fluid(Block block, Material bucket) { this.defaultBlock = block; this.bucket = ItemStack.of(bucket); } public Block getDefaultBlock() { return defaultBlock; } public ItemStack getBucket() { return bucket; } protected abstract boolean canBeReplacedWith(Instance instance, Point point, Fluid other, Direction direction); public abstract int getNextTickDelay(Instance instance, Point point, Block block); public void onTick(Instance instance, Point point, Block block) { } protected boolean isEmpty() { return false; } protected abstract double getBlastResistance(); public abstract double getHeight(Block block, Instance instance, Point point); public abstract double getHeight(Block block); public static boolean isSource(Block block) { String levelStr = block.getProperty("level"); return levelStr == null || Integer.parseInt(levelStr) == 0; } public static int getLevel(Block block) { String levelStr = block.getProperty("level"); if (levelStr == null) return 8; int level = Integer.parseInt(levelStr); if (level == 0) return 8; // Source block return level; } public static boolean isFalling(Block block) { String levelStr = block.getProperty("level"); if (levelStr == null) return false; return Integer.parseInt(levelStr) >= 8; } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/FluidPlacementRule.java ================================================ package io.github.togar2.fluids; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.rule.BlockPlacementRule; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class FluidPlacementRule extends BlockPlacementRule { public FluidPlacementRule(@NotNull Block block) { super(block); } @Override public @Nullable Block blockPlace(@NotNull PlacementState placementState) { if (placementState.instance() instanceof Instance instance) { MinestomFluids.scheduleTick(instance, placementState.placePosition(), placementState.block()); } return placementState.block(); } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/FluidSimulationFeature.java ================================================ package io.github.togar2.fluids; import net.kyori.adventure.key.Key; import net.minestom.vanilla.BlockUpdateFeature; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; import java.util.Set; public class FluidSimulationFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { // TODO: Use the block-update-system MinestomFluids.init(context.vri().process()); } @Override public @NotNull Key key() { return Key.key("io.github.togar2:fluids"); } @NotNull public Set> dependencies() { return Set.of(BlockUpdateFeature.class); } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/MinestomFluids.java ================================================ package io.github.togar2.fluids; import net.minestom.server.ServerProcess; import net.minestom.server.coordinate.Point; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class MinestomFluids { public static final Fluid WATER = new WaterFluid(); public static final Fluid EMPTY = new EmptyFluid(); private static final Map>> UPDATES = new ConcurrentHashMap<>(); public static Fluid get(Block block) { if (block.compare(Block.WATER)) { return WATER; } else if (block.compare(Block.LAVA)) { return EMPTY; } else { return EMPTY; } } public static void tick(InstanceTickEvent event) { Set currentUpdate = UPDATES.computeIfAbsent(event.getInstance(), i -> new ConcurrentHashMap<>()) .get(event.getInstance().getWorldAge()); if (currentUpdate == null) return; for (Point point : currentUpdate) { tick(event.getInstance(), point); } UPDATES.get(event.getInstance()).remove(event.getInstance().getWorldAge()); } public static void tick(Instance instance, Point point) { get(instance.getBlock(point)).onTick(instance, point, instance.getBlock(point)); } public static void scheduleTick(Instance instance, Point point, Block block) { int tickDelay = MinestomFluids.get(block).getNextTickDelay(instance, point, block); if (tickDelay == -1) return; //TODO figure out a way to remove instance from map if unregistered? long newAge = instance.getWorldAge() + tickDelay; UPDATES.get(instance).computeIfAbsent(newAge, l -> new HashSet<>()).add(point); } public static void init(ServerProcess process) { process.block().registerBlockPlacementRule(new FluidPlacementRule(Block.WATER)); process.eventHandler().addChild(events()); } public static EventNode events() { EventNode node = EventNode.all("fluid-events"); node.addListener(InstanceTickEvent.class, MinestomFluids::tick); return node; } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/WaterBlockBreakEvent.java ================================================ package io.github.togar2.fluids; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.event.trait.BlockEvent; import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; public class WaterBlockBreakEvent implements InstanceEvent, BlockEvent, CancellableEvent { private final Instance instance; private final BlockVec blockPosition; private final Block block; private boolean cancelled; public WaterBlockBreakEvent(@NotNull Instance instance, @NotNull BlockVec blockPosition, @NotNull Block block) { this.instance = instance; this.blockPosition = blockPosition; this.block = block; } @Override public @NotNull Instance getInstance() { return instance; } public @NotNull BlockVec getBlockPosition() { return blockPosition; } @Override public @NotNull Block getBlock() { return block; } @Override public boolean isCancelled() { return cancelled; } @Override public void setCancelled(boolean cancel) { this.cancelled = cancel; } } ================================================ FILE: fluid-simulation/src/main/java/io/github/togar2/fluids/WaterFluid.java ================================================ package io.github.togar2.fluids; import net.minestom.server.coordinate.BlockVec; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.Material; import net.minestom.server.utils.Direction; public class WaterFluid extends FlowableFluid { public WaterFluid() { super(Block.WATER, Material.WATER_BUCKET); } @Override protected boolean isInfinite() { return true; } @Override protected boolean onBreakingBlock(Instance instance, Point point, Block block) { WaterBlockBreakEvent event = new WaterBlockBreakEvent(instance, new BlockVec(point), block); return !event.isCancelled(); } @Override protected int getHoleRadius(Instance instance) { return 4; } @Override public int getLevelDecreasePerBlock(Instance instance) { return 1; } @Override public int getTickRate(Instance instance) { return 5; } @Override protected boolean canBeReplacedWith(Instance instance, Point point, Fluid other, Direction direction) { return direction == Direction.DOWN && this == other; } @Override protected double getBlastResistance() { return 100; } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ minestom_version=2025.07.04-1.21.5 rayfast_version=684e854a48 jnoise_version=4.0.0 annotations_version=23.0.0 window_version=1.1 slf4j_version=2.0.16 ================================================ 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/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # 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. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: instance-meta/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: instance-meta/src/main/java/net/minestom/vanilla/instancemeta/InstanceMetaFeature.java ================================================ package net.minestom.vanilla.instancemeta; import net.kyori.adventure.key.Key; import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.instance.Instance; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.instancemeta.tickets.TicketManager; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.WeakHashMap; public class InstanceMetaFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { new Logic().hook(context.vri()); } @Override public @NotNull Key key() { return Key.key("vri:instancemeta"); } private static class Logic { private final @NotNull Map instance2TicketManager = Collections.synchronizedMap(new WeakHashMap<>()); private Logic() { } private void hook(@NotNull VanillaReimplementation vri) { vri.process().eventHandler().addListener(InstanceTickEvent.class, event -> tickInstance(event.getInstance())); } // Process all future tickets private void tickInstance(@NotNull Instance instance) { TicketManager ticketManager = instance2TicketManager.computeIfAbsent(instance, ignored -> new TicketManager()); List waitingForceLoads = instance.getTag(TicketManager.WAITING_TICKETS_TAG); if (waitingForceLoads == null) { return; } for (TicketManager.Ticket waitingForceLoad : waitingForceLoads) { ticketManager.addTicket(waitingForceLoad); } instance.setTag(TicketManager.WAITING_TICKETS_TAG, List.of()); } } } ================================================ FILE: instance-meta/src/main/java/net/minestom/vanilla/instancemeta/tickets/TicketManager.java ================================================ package net.minestom.vanilla.instancemeta.tickets; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ShortMap; import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ShortMap; import it.unimi.dsi.fastutil.objects.Object2ShortOpenHashMap; import it.unimi.dsi.fastutil.shorts.Short2IntMap; import it.unimi.dsi.fastutil.shorts.Short2IntOpenHashMap; import net.minestom.server.coordinate.CoordConversion; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagReadable; import net.minestom.server.tag.TagSerializer; import net.minestom.server.tag.TagWritable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.stream.Collectors; /* Tickets are used to choose when & how to load chunks, when and how to tick entities, and when and how to tick BlockHandlers. Tickets range: Inaccessible: 0 and below -> No game aspects are active, but world generation still occurs. Border: 1 -> Only some game aspects are active (Redstone and command blocks do not work). Ticking: 2 -> All game aspects are active except that entities are not processed (do not move) and chunk ticks aren't either. Entity Ticking: 3 and above -> All game aspects are active. Vanilla Ticket Value -> vri Ticket Value: vriTicketValue = 34 - vanillaTicketValue */ /** * A utility class used to manage instance's tickets */ @SuppressWarnings("UnstableApiUsage") public class TicketManager { public static final Tag> WAITING_TICKETS_TAG = Tag.Structure("vri:instancemeta:waiting_tickets", TicketManager.Ticket.SERIALIZER) .list(); public static final Tag> REMOVING_TICKETS_TAG = Tag.Structure("vri:instancemeta:removing_tickets", TicketManager.Ticket.SERIALIZER) .list(); // Vanilla ticket values public static final short PLAYER_TICKET = 34 - 31; public static final short FORCED_TICKET = 34 - 31; public static final short START_TICKET = 34 - 22; public static final short PORTAL_TICKET = 34 - 30; public static final short DRAGON_TICKET = 34 - 24; public static final short POST_TELEPORT_TICKET = 34 - 33; public static final short SPREAD_PLAYERS_TICKET = 34 - 33; public static final short END_PORTAL_TICKET = 34 - 33; public static final short TEMPORARY_TICKET = 34 - 33; /** * The first Long is the target chunk * The second Long is the source chunk * The resulting Short is the value for this chunk */ private final Object2ShortMap externalTicketValues = new Object2ShortOpenHashMap<>(); private final Long2ObjectMap internalTicketValues = new Long2ObjectOpenHashMap<>(); private final Long2ShortMap currentTicketValue = new Long2ShortOpenHashMap(); public interface Ticket { short value(); long chunk(); static @NotNull Ticket from(short value, long chunk) { return new TicketImpl(value, chunk); } Tag VALUE_TAG = Tag.Short("vri:instancemeta:ticket_value"); Tag CHUNK_TAG = Tag.Long("vri:instancemeta:ticket_chunk"); TagSerializer SERIALIZER = new TagSerializer<>() { @Override public @Nullable Ticket read(@NotNull TagReadable reader) { Short value = reader.getTag(VALUE_TAG); Long chunk = reader.getTag(CHUNK_TAG); if (value == null || chunk == null) { return null; } return new TicketImpl(value, chunk); } @Override public void write(@NotNull TagWritable writer, @NotNull Ticket value) { writer.setTag(VALUE_TAG, value.value()); writer.setTag(CHUNK_TAG, value.chunk()); } }; } private record TicketImpl(short value, long chunk) implements Ticket { } private record Source2Target(long source, long target) { } public TicketManager() { } // Ticket methods /** * Adds a ticket and updates surrounding chunks. * * @param ticket the ticket to add */ public void addTicket(@NotNull Ticket ticket) { addTicket(ticket.value(), ticket.chunk()); } /** * Adds a ticket to the specified chunk and updates surrounding chunks. * * @param chunk the chunk index of the chunk to add the ticket to * @param value the value of the ticket */ public void addTicket(short value, long chunk) { // Add value to internal Short2IntMap internalValues = internalTicketValues.get(chunk); internalValues.putIfAbsent(value, 0); internalValues.put(value, internalValues.get(value) + 1); externalTicketValues.put(new Source2Target(chunk, chunk), value); recalculateChunkValue(chunk); // Add values to surrounding external short currentSourceValue = value; int originX = CoordConversion.chunkIndexGetX(chunk); int originZ = CoordConversion.chunkIndexGetZ(chunk); while (currentSourceValue > 1) { // Find starting positions + starting external value int halfWidth = currentSourceValue - 1; short externalValue = (short) (value - currentSourceValue + 1); // Left starting position int leftX = originX - halfWidth; int leftZ = originZ + halfWidth; // Top starting position int topX = originX - halfWidth; int topZ = originZ - halfWidth; // Right starting position int rightX = originX + halfWidth; int rightZ = originZ - halfWidth; // Down starting position int downX = originX + halfWidth; int downZ = originZ + halfWidth; // Do all squares for (int offset = 0; offset < ((halfWidth * 2) + 1); offset++) { { // Left side long chunkIndex = CoordConversion.chunkIndex(leftX, leftZ - offset); externalTicketValues.put(new Source2Target(chunkIndex, chunk), externalValue); recalculateChunkValue(chunkIndex); } { // Top side long chunkIndex = CoordConversion.chunkIndex(topX + offset, topZ); externalTicketValues.put(new Source2Target(chunkIndex, chunk), externalValue); recalculateChunkValue(chunkIndex); } { // Right side long chunkIndex = CoordConversion.chunkIndex(rightX, rightZ + offset); externalTicketValues.put(new Source2Target(chunkIndex, chunk), externalValue); recalculateChunkValue(chunkIndex); } { // Downside long chunkIndex = CoordConversion.chunkIndex(downX - offset, downZ); externalTicketValues.put(new Source2Target(chunkIndex, chunk), externalValue); recalculateChunkValue(chunkIndex); } } currentSourceValue--; } } /** * Removes a ticket from this chunk and updates the surrounding chunks * * @param chunk the chunk index of the chunk to remove the ticket from * @param value the value of the ticket being removed */ public void removeTicket(long chunk, short value) { // Remove value from internal Short2IntMap internalValues = internalTicketValues.get(chunk); if (internalValues.containsKey(value)) { int current = internalValues.get(value); if (current == 1) { internalValues.remove(value); } else { internalValues.put(value, current - 1); } return; } // Remove value from external access into this chunk externalTicketValues.removeShort(new Source2Target(chunk, chunk)); recalculateChunkValue(chunk); // Remove values from surrounding external short highestInternalValue = 0; for (short internalValue : internalValues.keySet()) { if (internalValue > highestInternalValue) { highestInternalValue = internalValue; } } if (highestInternalValue > 0) { externalTicketValues.put(new Source2Target(chunk, chunk), highestInternalValue); } short previousSourceValue = value; int originX = CoordConversion.chunkIndexGetX(chunk); int originZ = CoordConversion.chunkIndexGetZ(chunk); while (previousSourceValue > 1) { // Find starting positions + starting external value int halfWidth = previousSourceValue - 1; // Left starting position int leftX = originX - halfWidth; int leftZ = originZ + halfWidth; // Top starting position int topX = originX - halfWidth; int topZ = originZ - halfWidth; // Right starting position int rightX = originX + halfWidth; int rightZ = originZ - halfWidth; // Down starting position int downX = originX + halfWidth; int downZ = originZ + halfWidth; // Do all squares for (int offset = 0; offset < ((halfWidth * 2) + 1); offset++) { { // Left side long chunkIndex = CoordConversion.chunkIndex(leftX, leftZ - offset); if (highestInternalValue <= 0) { externalTicketValues.removeShort(new Source2Target(chunkIndex, chunk)); } else { externalTicketValues.put(new Source2Target(chunkIndex, chunk), highestInternalValue); } recalculateChunkValue(chunkIndex); } { // Top side long chunkIndex = CoordConversion.chunkIndex(topX + offset, topZ); if (highestInternalValue <= 0) { externalTicketValues.removeShort(new Source2Target(chunkIndex, chunk)); } else { externalTicketValues.put(new Source2Target(chunkIndex, chunk), highestInternalValue); } recalculateChunkValue(chunkIndex); } { // Right side long chunkIndex = CoordConversion.chunkIndex(rightX, rightZ + offset); if (highestInternalValue <= 0) { externalTicketValues.removeShort(new Source2Target(chunkIndex, chunk)); } else { externalTicketValues.put(new Source2Target(chunkIndex, chunk), highestInternalValue); } recalculateChunkValue(chunkIndex); } { // Downside long chunkIndex = CoordConversion.chunkIndex(downX - offset, downZ); if (highestInternalValue <= 0) { externalTicketValues.removeShort(new Source2Target(chunkIndex, chunk)); } else { externalTicketValues.put(new Source2Target(chunkIndex, chunk), highestInternalValue); } recalculateChunkValue(chunkIndex); } } previousSourceValue--; highestInternalValue--; } } /** * Gets the ticket value of the specified chunk. * * @param chunkIndex the chunk index of the chunk to retrieve the ticket value from * @return the ticket value */ public short getTicketValue(long chunkIndex) { return currentTicketValue.get(chunkIndex); } /** * Gets information on the tickets for this specified chunk * * @param chunkIndex the chunk index of the chunk to retrieve the ticket info from * @return the ticket value */ public String getChunkInfo(long chunkIndex) { return "Current Value: " + currentTicketValue.get(chunkIndex) + "\n" + "Internal Tickets: " + internalTicketValues.get(chunkIndex) + "\n" + "External Tickets: " + " (" + externalTicketValues.object2ShortEntrySet() .stream() .filter(entry -> entry.getKey().target() == chunkIndex) .map(String::valueOf) .collect(Collectors.joining(", ")) + " )"; } private void handleInstanceChunkLoad(long chunkIndex) { synchronized (this) { prepareChunk(chunkIndex); } } private void prepareChunk(long chunk) { currentTicketValue.computeIfAbsent(chunk, ignored -> (short) 0); internalTicketValues.computeIfAbsent(chunk, k -> new Short2IntOpenHashMap()); } private static class MutableShort { private short value; } private void recalculateChunkValue(long chunkIndex) { prepareChunk(chunkIndex); MutableShort highest = new MutableShort(); highest.value = 0; externalTicketValues.forEach((source2Target, value) -> { if (source2Target.target() == chunkIndex) { if (value > highest.value) { highest.value = value; } } }); // Set new value this.currentTicketValue.put(chunkIndex, highest.value); } } ================================================ FILE: instance-meta/src/main/java/net/minestom/vanilla/instancemeta/tickets/TicketUtils.java ================================================ package net.minestom.vanilla.instancemeta.tickets; import net.minestom.server.instance.Instance; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.List; import java.util.stream.Stream; public class TicketUtils { public static @NotNull List waitingTickets(@NotNull Instance instance) { return instance.getTag(TicketManager.WAITING_TICKETS_TAG); } public static void waitingTickets(@NotNull Instance instance, @NotNull Collection ticketsToAdd) { List newWaitingTickets = Stream.concat( waitingTickets(instance).stream(), ticketsToAdd.stream() ).toList(); instance.setTag(TicketManager.WAITING_TICKETS_TAG, newWaitingTickets); } public static @NotNull List removingTickets(Instance instance) { return instance.getTag(TicketManager.REMOVING_TICKETS_TAG); } public static void removingTickets(Instance instance, @NotNull Collection from) { List newRemovingTickets = Stream.concat( removingTickets(instance).stream(), from.stream() ).toList(); instance.setTag(TicketManager.REMOVING_TICKETS_TAG, newRemovingTickets); } } ================================================ FILE: item-placeables/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: item-placeables/src/main/java/net/minestom/vanilla/itemplaceables/ItemPlaceablesFeature.java ================================================ package net.minestom.vanilla.itemplaceables; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ItemPlaceablesFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { // TODO: Flesh this out and make it configurable Map itemPlaceables = new ConcurrentHashMap<>(); itemPlaceables.put(Material.WATER_BUCKET, Block.WATER); itemPlaceables.put(Material.LAVA_BUCKET, Block.LAVA); context.vri().process().eventHandler().addListener(PlayerUseItemOnBlockEvent.class, event -> { Point position = event.getPosition(); var face = event.getBlockFace(); ItemStack item = event.getItemStack(); Block block = itemPlaceables.get(item.material()); if (block == null) return; position = position.relative(face); event.getInstance().setBlock(position, block); }); } @Override public @NotNull Key key() { return Key.key("vri:item-placeables"); } } ================================================ FILE: items/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: items/src/main/java/net/minestom/vanilla/items/FlintAndSteelHandler.java ================================================ package net.minestom.vanilla.items; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerHand; import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.item.ItemStack; import net.minestom.server.utils.Direction; import net.minestom.vanilla.inventory.InventoryManipulation; public class FlintAndSteelHandler implements VanillaItemHandler { public FlintAndSteelHandler() { } @Override public boolean onUseOnBlock(PlayerUseItemOnBlockEvent event) { // TODO: check if flammable Point pos = event.getPosition(); Player player = event.getPlayer(); Instance instance = player.getInstance(); ItemStack itemStack = event.getItemStack(); PlayerHand hand = event.getHand(); Direction blockDir = event.getBlockFace().toDirection(); // Find block in direction Point firePosition = pos.add( blockDir.normalX(), blockDir.normalY(), blockDir.normalZ() ); Block atFirePosition = instance.getBlock(firePosition); if (atFirePosition.isAir()) { InventoryManipulation.damageItemIfNotCreative(player, hand, 1); // Block block, Instance instance, Point blockPosition, Player player, Player.Hand hand, // BlockFace blockFace, float cursorX, float cursorY, float cursorZ instance.placeBlock(new BlockHandler.PlayerPlacement( Block.FIRE, instance, firePosition, player, hand, event.getBlockFace(), 0, 0, 0 // TODO: cursor position via raycast )); return true; } return false; } } ================================================ FILE: items/src/main/java/net/minestom/vanilla/items/ItemManager.java ================================================ package net.minestom.vanilla.items; import net.minestom.server.event.Event; import net.minestom.server.event.EventListener; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; public class ItemManager { public static @NotNull ItemManager accumulate(@NotNull Consumer accumulator) { Map itemHandlersByMaterial = new HashMap<>(); accumulator.accept(itemHandlersByMaterial::put); return new ItemManager(itemHandlersByMaterial); } public interface Accumulator { void accumulate(@NotNull Material material, @NotNull VanillaItemHandler itemHandler); } private final Map itemHandlersByMaterial; private ItemManager(Map itemHandlersByMaterial) { this.itemHandlersByMaterial = Map.copyOf(itemHandlersByMaterial); } private void handlePlayerUseItemEvent(PlayerUseItemEvent event) { ItemStack itemStack = event.getItemStack(); VanillaItemHandler itemHandler = itemHandlersByMaterial.get(itemStack.material()); if (itemHandler == null) { return; } itemHandler.onUseInAir(event); } private void handlePlayerUseItemOnBlockEvent(PlayerUseItemOnBlockEvent event) { ItemStack itemStack = event.getItemStack(); VanillaItemHandler itemHandler = itemHandlersByMaterial.get(itemStack.material()); if (itemHandler == null) { return; } itemHandler.onUseOnBlock(event); } public void registerEvents(EventNode itemEventNode) { itemEventNode.addListener( EventListener.of(PlayerUseItemEvent.class, this::handlePlayerUseItemEvent) ); itemEventNode.addListener( EventListener.of(PlayerUseItemOnBlockEvent.class, this::handlePlayerUseItemOnBlockEvent) ); } } ================================================ FILE: items/src/main/java/net/minestom/vanilla/items/ItemsFeature.java ================================================ package net.minestom.vanilla.items; import net.kyori.adventure.key.Key; import net.minestom.vanilla.VanillaReimplementation; import org.jetbrains.annotations.NotNull; public class ItemsFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { ItemManager manager = ItemManager.accumulate(accumulator -> { for (VanillaItems item : VanillaItems.values()) { accumulator.accumulate(item.getMaterial(), item.getItemHandlerSupplier().get()); } }); manager.registerEvents(context.vri().process().eventHandler()); } @Override public @NotNull Key key() { return Key.key("vri:items"); } } ================================================ FILE: items/src/main/java/net/minestom/vanilla/items/VanillaItemHandler.java ================================================ package net.minestom.vanilla.items; import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; public interface VanillaItemHandler { /** * Called when the player right clicks with this item in the air * * @param event the event object */ default void onUseInAir(PlayerUseItemEvent event) { } /** * Called when the player right clicks with this item on a block * * @param event the event object * @return true if it prevents normal item use (placing blocks for instance) */ default boolean onUseOnBlock(PlayerUseItemOnBlockEvent event) { return false; } } ================================================ FILE: items/src/main/java/net/minestom/vanilla/items/VanillaItems.java ================================================ package net.minestom.vanilla.items; import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * All items with special behaviour available in the vanilla reimplementation */ public enum VanillaItems { FLINT_AND_STEEL(Material.FLINT_AND_STEEL, FlintAndSteelHandler::new); private final Material material; private final Supplier itemCreator; VanillaItems(@NotNull Material material, Supplier itemCreator) { this.itemCreator = itemCreator; this.material = material; } public @NotNull Material getMaterial() { return material; } public @NotNull Supplier getItemHandlerSupplier() { return itemCreator; } } ================================================ FILE: jitpack.yml ================================================ jdk: - openjdk21 ================================================ FILE: loot-table/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":datapack")) } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/BlockExperience.java ================================================ package net.minestom.vanilla.loot; import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minestom.server.component.DataComponents; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.random.RandomGenerator; /** * Utilities for calculating how much experience ought to be dropped when a block is broken. */ public class BlockExperience { /** * Gets the amount of experience the given block should drop when mined. */ public static int getExperience(@NotNull Block minedBlock, @NotNull ItemStack tool, @NotNull RandomGenerator random) { // TODO: Will have to take into account EffectComponent#BLOCK_EXPERIENCE on Enchantments if it is ever used. if (tool.get(DataComponents.ENCHANTMENTS, EnchantmentList.EMPTY).has(Enchantment.SILK_TOUCH)) return 0; return switch (EXPERIENCE.get(minedBlock.id())) { case Amount.Constant(int points) -> points; case Amount.Uniform(int min, int max) -> random.nextInt(min, max + 1); case null -> 0; }; } /** * An amount of experience - either a constant or uniform. */ public sealed interface Amount { record Constant(int amount) implements Amount {} record Uniform(int min, int max) implements Amount {} } /** * Unfortunately, this cannot be datagenned. This is not a consistent property in the code, and cannot be reliably * checked. Naive checks will detect most of these blocks (but not all), and will label some other blocks as * constant 0. Implementing this correctly requires code introspection abilities that are not reasonable to * implement, especially given the small number (16) of blocks that actually drop experience. */ private static final @NotNull Int2ObjectMap EXPERIENCE = new Int2ObjectArrayMap<>(Map.ofEntries( // Ores entry(Block.COAL_ORE, new Amount.Uniform(0, 2)), entry(Block.DEEPSLATE_COAL_ORE, new Amount.Uniform(0, 2)), entry(Block.LAPIS_ORE, new Amount.Uniform(2, 5)), entry(Block.DEEPSLATE_LAPIS_ORE, new Amount.Uniform(2, 5)), entry(Block.REDSTONE_ORE, new Amount.Uniform(1, 5)), entry(Block.DEEPSLATE_REDSTONE_ORE, new Amount.Uniform(1, 5)), entry(Block.DIAMOND_ORE, new Amount.Uniform(3, 7)), entry(Block.DEEPSLATE_DIAMOND_ORE, new Amount.Uniform(3, 7)), entry(Block.EMERALD_ORE, new Amount.Uniform(3, 7)), entry(Block.DEEPSLATE_EMERALD_ORE, new Amount.Uniform(3, 7)), entry(Block.NETHER_GOLD_ORE, new Amount.Uniform(0, 1)), entry(Block.NETHER_QUARTZ_ORE, new Amount.Uniform(2, 5)), // Sculk entry(Block.SCULK, new Amount.Constant(1)), entry(Block.SCULK_SHRIEKER, new Amount.Constant(5)), entry(Block.SCULK_SENSOR, new Amount.Constant(5)), entry(Block.SCULK_CATALYST, new Amount.Constant(5)) )); private static Map.Entry entry(Block block, Amount amount) { return Map.entry(block.id(), amount); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootContext.java ================================================ package net.minestom.vanilla.loot; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.Random; /** * Stores a dynamic amount of information that may be relevant during the generation of loot. */ public sealed interface LootContext permits LootContextImpl { @NotNull LootContext.Key RANDOM = LootContext.key("minecraft:random"); @NotNull LootContext.Key EXPLOSION_RADIUS = LootContext.key("minecraft:explosion_radius"); @NotNull LootContext.Key LAST_DAMAGE_PLAYER = LootContext.key("minecraft:last_damage_player"); @NotNull LootContext.Key WORLD = LootContext.key("minecraft:world"); @NotNull LootContext.Key TOOL = LootContext.key("minecraft:tool"); @NotNull LootContext.Key ENCHANTMENT_ACTIVE = LootContext.key("minecraft:enchantment_active"); @NotNull LootContext.Key BLOCK_STATE = LootContext.key("minecraft:block_state"); @NotNull LootContext.Key DAMAGE_SOURCE = LootContext.key("minecraft:damage_source"); @NotNull LootContext.Key ORIGIN = LootContext.key("minecraft:origin"); @NotNull LootContext.Key DIRECT_ATTACKING_ENTITY = LootContext.key("minecraft:direct_attacking_entity"); @NotNull LootContext.Key ATTACKING_ENTITY = LootContext.key("minecraft:attacking_entity"); @NotNull LootContext.Key THIS_ENTITY = LootContext.key("minecraft:this_entity"); @NotNull LootContext.Key LUCK = LootContext.key("minecraft:luck"); @NotNull LootContext.Key ENCHANTMENT_LEVEL = LootContext.key("minecraft:enchantment_level"); /** * Creates a loot context from the provided map of key -> object. * @param data the values of the context * @return the new context instance */ static @NotNull LootContext from(@NotNull Map, Object> data) { return LootContextImpl.from(data); } /** * Creates a key from the provided key. */ static LootContext.@NotNull Key key(@NotNull String key) { return new LootContext.Key<>(key); } /** * Represents a key that stores information in a loot context. * @param id the string id of the key * @param the type parameter of the key */ @SuppressWarnings("unused") record Key(@NotNull String id) {} /** * Returns whether or not this context has the provided key. * @param key the key to search for * @return true if this context has the key, false if not */ boolean has(@NotNull Key key); /** * Gets the object associated with the provided key, returning null if not. * @param key the key to search for * @return the optional value * @param the type of object desired */ @Nullable T get(@NotNull Key key); /** * Gets the object associated with the provided key, returning the default value if not. * @param key the key to search for * @param defaultValue the default value to use * @return the optional value * @param the type of object desired */ @NotNull T get(@NotNull Key key, @NotNull T defaultValue); /** * Gets the object associated with the provided key, throwing an exception if not. * @param key the key to search for * @return the object associated with the provided key * @param the type of object desired */ @NotNull T require(@NotNull Key key); } record LootContextImpl(@NotNull Map data) implements LootContext { LootContextImpl { data = Map.copyOf(data); } static @NotNull LootContext from(@NotNull Map, Object> data) { Map mapped = new HashMap<>(); for (Map.Entry, Object> entry : data.entrySet()) { if (entry.getValue() == null) continue; mapped.put(entry.getKey().id(), entry.getValue()); } return new LootContextImpl(mapped); } @Override public boolean has(@NotNull Key key) { return data.containsKey(key.id()); } @SuppressWarnings("unchecked") @Override public @Nullable T get(@NotNull Key key) { return (T) data.get(key.id()); } @Override public @NotNull T get(@NotNull Key key, @NotNull T defaultValue) { T get = get(key); return get != null ? get : defaultValue; } @Override public @NotNull T require(@NotNull Key key) { T get = get(key); if (get != null) { return get; } throw new NoSuchElementException("No value for key '" + key + "'"); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootEntry.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.minestom.server.MinecraftServer; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.registry.Registries; import net.minestom.server.registry.RegistryKey; import net.minestom.server.registry.RegistryTag; import net.minestom.server.utils.Either; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Range; import java.util.ArrayList; import java.util.List; /** * An entry in a loot table that can generate a list of {@link Choice choices} that each have their own loot and weight. */ @SuppressWarnings("UnstableApiUsage") public interface LootEntry { @NotNull StructCodec CODEC = Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootEntry::codec, "type"); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_entries")); registry.register("alternatives", Alternatives.CODEC); registry.register("dynamic", Dynamic.CODEC); registry.register("empty", Empty.CODEC); registry.register("group", Group.CODEC); registry.register("item", Item.CODEC); registry.register("loot_table", LootTable.CODEC); registry.register("sequence", Sequence.CODEC); registry.register("tag", Tag.CODEC); return registry; } /** * Generates any number of possible choices to choose from when generating loot. * @param context the context object, to use if required * @return a list, with undetermined mutability, containing the options that were generated */ @NotNull List requestChoices(@NotNull LootContext context); /** * @return the codec that can encode this entry */ @NotNull StructCodec codec(); /** * A choice, generated from an entry, that could potentially be chosen. */ interface Choice extends LootGenerator { /** * Calculates the weight of this choice, to be used when choosing which choices should be used. * This number should not be below 1.
* When using the result of this method, be aware of the fact that it's valid for implementations of this method * to return different values even when the provided context is the identical. * @param context the context object, to use if required * @return the weight of this choice */ @Range(from = 1L, to = Long.MAX_VALUE) long getWeight(@NotNull LootContext context); /** * A choice that uses the standard method of generating weight - adding the {@link #weight()} to the {@link #quality()} * where the quality is multiplied by the provided context's luck ({@link LootContext#LUCK}). */ interface Standard extends Choice { /** * The weight of this choice. When calculating the final weight, this value is simply added to the result. * @return the base weight of this choice */ @Range(from = 1L, to = Long.MAX_VALUE) long weight(); /** * The quality of the choice. When calculating the final weight, this number is multiplied by the context's luck * value, which is stored at the key {@link LootContext#LUCK}. * @return the quality of the choice */ @Range(from = 0L, to = Long.MAX_VALUE) long quality(); @Override default @Range(from = 1L, to = Long.MAX_VALUE) long getWeight(@NotNull LootContext context) { return Math.max(1, (long) Math.floor(weight() + quality() * context.get(LootContext.LUCK, 0d))); } } /** * A standard single choice entry that only returns itself when its conditions all succeed. */ interface Single extends LootEntry, LootEntry.Choice, Standard { /** * @return this choice's predicates */ @NotNull List predicates(); /** * Requests choices, returning none if {@link #predicates()} are all true. * {@inheritDoc} */ @Override default @NotNull List requestChoices(@NotNull LootContext context) { return LootPredicate.all(predicates(), context) ? List.of(this) : List.of(); } } } record Alternatives(@NotNull List predicates, @NotNull List children) implements LootEntry { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Alternatives::predicates, "children", LootEntry.CODEC.list().optional(List.of()), Alternatives::children, Alternatives::new ); @Override public @NotNull List requestChoices(@NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return List.of(); for (var entry : this.children()) { var options = entry.requestChoices(context); if (!options.isEmpty()) { return options; } } return List.of(); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Dynamic(@NotNull List predicates, @NotNull List functions, long weight, long quality, @NotNull Key name) implements Choice.Single { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Dynamic::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), Dynamic::functions, "weight", Codec.LONG.optional(1L), Dynamic::weight, "quality", Codec.LONG.optional(0L), Dynamic::quality, "name", Codec.KEY, Dynamic::name, Dynamic::new ); private static final net.minestom.server.tag.Tag> DECORATED_POT_SHERDS = net.minestom.server.tag.Tag.String("sherds") .map(Key::key, Key::asString) .map(Material::fromKey, Material::key) .list().defaultValue(List::of); private static final net.minestom.server.tag.Tag> CONTAINER_ITEMS = net.minestom.server.tag.Tag.ItemStack("Items").list().defaultValue(List::of); @Override public @NotNull List generate(@NotNull LootContext context) { Block block = context.get(LootContext.BLOCK_STATE); if (block == null) return List.of(); return switch (name.asString()) { case "minecraft:sherds" -> { List items = new ArrayList<>(); for (Material material : block.getTag(DECORATED_POT_SHERDS)) { items.add(ItemStack.of(material)); } yield items; } case "minecraft:contents" -> block.getTag(CONTAINER_ITEMS); default -> List.of(); }; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Empty(@NotNull List predicates, @NotNull List functions, long weight, long quality) implements Choice.Single { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Empty::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), Empty::functions, "weight", Codec.LONG.optional(1L), Empty::weight, "quality", Codec.LONG.optional(0L), Empty::quality, Empty::new ); @Override public @NotNull List generate(@NotNull LootContext context) { return List.of(); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Group(@NotNull List predicates, @NotNull List children) implements LootEntry { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Group::predicates, "children", LootEntry.CODEC.list().optional(List.of()), Group::children, Group::new ); @Override public @NotNull List requestChoices(@NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return List.of(); List choices = new ArrayList<>(); for (var entry : this.children()) { choices.addAll(entry.requestChoices(context)); } return choices; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Item(@NotNull List predicates, @NotNull List functions, long weight, long quality, @NotNull Material name) implements Choice.Single { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Item::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), Item::functions, "weight", Codec.LONG.optional(1L), Item::weight, "quality", Codec.LONG.optional(0L), Item::quality, "name", Material.CODEC, Item::name, Item::new ); @Override public @NotNull List generate(@NotNull LootContext context) { return List.of(LootFunction.apply(functions, ItemStack.of(name), context)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record LootTable(@NotNull List predicates, @NotNull List functions, long weight, long quality, @NotNull Either value) implements Choice.Single { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), LootTable::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), LootTable::functions, "weight", Codec.LONG.optional(1L), LootTable::weight, "quality", Codec.LONG.optional(0L), LootTable::quality, "value", Codec.Either(Codec.KEY, net.minestom.vanilla.loot.LootTable.CODEC), LootTable::value, LootTable::new ); @Override public @NotNull List generate(@NotNull LootContext context) { var table = switch (value) { case Either.Left(Key key) -> throw new UnsupportedOperationException("TODO: Implement loot table registry (Key -> @Nullable LootTable)"); case Either.Right(net.minestom.vanilla.loot.LootTable right) -> right; }; if (table == null) return List.of(); return LootFunction.apply(functions, table.generate(context), context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Sequence(@NotNull List predicates, @NotNull List children) implements LootEntry { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Sequence::predicates, "children", LootEntry.CODEC.list().optional(List.of()), Sequence::children, Sequence::new ); @Override public @NotNull List requestChoices(@NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return List.of(); List options = new ArrayList<>(); for (var entry : this.children()) { var choices = entry.requestChoices(context); if (choices.isEmpty()) { break; } options.addAll(choices); } return options; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Tag(@NotNull List predicates, @NotNull List functions, long weight, long quality, @NotNull RegistryTag name, boolean expand) implements Choice.Single { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Tag::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), Tag::functions, "weight", Codec.LONG.optional(1L), Tag::weight, "quality", Codec.LONG.optional(0L), Tag::quality, "name", RegistryTag.codec(Registries::material), Tag::name, "expand", Codec.BOOLEAN, Tag::expand, Tag::new ); @Override public @NotNull List requestChoices(@NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) { return List.of(); } else if (!expand) { return List.of(this); } List choices = new ArrayList<>(); for (RegistryKey key : name) { Material material = MinecraftServer.process().material().get(key); if (material == null) continue; choices.add(new Choice() { @Override public @Range(from = 1L, to = Long.MAX_VALUE) long getWeight(@NotNull LootContext context) { return Tag.this.getWeight(context); } @Override public @NotNull List generate(@NotNull LootContext context) { return List.of(ItemStack.of(material)); } }); } return choices; } @Override public @NotNull List generate(@NotNull LootContext context) { List items = new ArrayList<>(); for (RegistryKey key : name) { Material material = MinecraftServer.process().material().get(key); if (material == null) continue; items.add(LootFunction.apply(functions, ItemStack.of(material), context)); } return items; } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootFeature.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.minestom.server.ServerProcess; import net.minestom.server.component.DataComponents; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.ExperienceOrb; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.ItemEntity; import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.component.Tool; import net.minestom.server.utils.time.TimeUnit; import net.minestom.vanilla.datapack.Datapacks; import net.minestom.vanilla.logging.Logger; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; import java.util.Map; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; public class LootFeature { public static @NotNull Map buildFromDatapack(@NotNull ServerProcess process) { final Path tablesPath = Path.of("/", "data", "minecraft", "loot_table"); Map tables; try { Path jar = Datapacks.ensureCurrentJarExists(); tables = Datapacks.buildRegistryFromJar(jar, tablesPath, process, ".json", LootTable.CODEC); } catch (IOException e) { throw new RuntimeException(e); } Logger.info("Loaded and parsed " + tables.size() + " loot tables"); return tables; } @SuppressWarnings("PatternValidation") public static @NotNull EventNode createEventNode(@NotNull Map tables) { return EventNode.type("vri:loot", EventFilter.INSTANCE).addListener(PlayerBlockBreakEvent.class, event -> { if (event.getPlayer().getGameMode() == GameMode.CREATIVE) return; // No loot in creative mode final Block block = event.getBlock(); ItemStack heldItem = event.getPlayer().getItemInMainHand(); Tool tool = heldItem.get(DataComponents.TOOL); // If the block doesn't require a tool, OR there is a tool and the block is explicitly allowed boolean canDrop = !block.registry().requiresTool() || (tool != null && tool.isCorrectForDrops(block)); if (!canDrop) return; // TODO: Can be pre-converted to `LootTable[]` that turns block IDs into loot tables. Key key = Key.key("blocks/" + block.key().value()); LootTable table = tables.get(key); if (table == null) { Logger.warn("Block " + block.key() + " does not have a corresponding loot table (would be at: " + key.asString() + ")"); return; } // Build a context and drop LootContext context = LootContext.from(Map.of( LootContext.RANDOM, new Random(), // TODO: Replace with sequence random LootContext.WORLD, event.getInstance(), LootContext.BLOCK_STATE, block, LootContext.ORIGIN, event.getBlockPosition(), LootContext.TOOL, heldItem, LootContext.THIS_ENTITY, event.getPlayer() )); for (ItemStack drop : table.generate(context)) { blockDrop(event.getInstance(), drop, event.getBlockPosition()); } int experience = BlockExperience.getExperience(block, heldItem, ThreadLocalRandom.current()); if (experience != 0) { ExperienceOrb orb = new ExperienceOrb((short) experience); orb.setInstance(event.getInstance(), event.getBlockPosition().add(0.5, 0.5, 0.5)); } }); } public static void blockDrop(@NotNull Instance instance, @NotNull ItemStack item, @NotNull Point block) { ThreadLocalRandom rng = ThreadLocalRandom.current(); Pos spawn = new Pos( block.blockX() + 0.5 + rng.nextDouble(-0.25, 0.25), block.blockY() + 0.5 + rng.nextDouble(-0.25, 0.25) - EntityType.ITEM.height() / 2, block.blockZ() + 0.5 + rng.nextDouble(-0.25, 0.25), rng.nextFloat(360), 0 ); drop(instance, item, spawn); } public static void drop(@NotNull Instance instance, @NotNull ItemStack item, @NotNull Point position) { ItemEntity entity = new ItemEntity(item); ThreadLocalRandom rng = ThreadLocalRandom.current(); Vec vel = new Vec( rng.nextDouble(-0.1, 0.1), 0.2, rng.nextDouble(-0.1, 0.1) ).mul(20); entity.setPickupDelay(10, TimeUnit.SERVER_TICK); entity.setInstance(instance, position); entity.setVelocity(vel); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootFunction.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.util.RGBLike; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.codec.Codec; import net.minestom.server.codec.Result; import net.minestom.server.codec.StructCodec; import net.minestom.server.codec.Transcoder; import net.minestom.server.color.Color; import net.minestom.server.component.DataComponent; import net.minestom.server.component.DataComponentMap; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.*; import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.entity.attribute.AttributeModifier; import net.minestom.server.entity.attribute.AttributeOperation; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.book.FilteredText; import net.minestom.server.item.component.*; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.item.instrument.Instrument; import net.minestom.server.potion.PotionEffect; import net.minestom.server.potion.PotionType; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.registry.Registries; import net.minestom.server.registry.RegistryKey; import net.minestom.server.registry.RegistryTag; import net.minestom.server.tag.Tag; import net.minestom.server.utils.Either; import net.minestom.vanilla.loot.util.*; import net.minestom.vanilla.loot.util.nbt.NBTPath; import net.minestom.vanilla.loot.util.nbt.NBTReference; import net.minestom.vanilla.loot.util.nbt.NBTUtils; import net.minestom.vanilla.loot.util.predicate.ItemPredicate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; /** * A function that allows loot to pass through it, potentially making modifications. */ @SuppressWarnings("UnstableApiUsage") public interface LootFunction { @NotNull Codec CODEC = makeCodec().orElse(makeCodec().list().transform(Sequence::new, seq -> ((Sequence) seq).functions())); private static StructCodec makeCodec() { return Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootFunction::codec, "function"); } static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_functions")); registry.register("apply_bonus", ApplyBonus.CODEC); registry.register("copy_components", CopyComponents.CODEC); registry.register("copy_custom_data", CopyCustomData.CODEC); registry.register("copy_name", CopyName.CODEC); registry.register("copy_state", CopyState.CODEC); registry.register("enchanted_count_increase", EnchantedCountIncrease.CODEC); registry.register("enchant_randomly", EnchantRandomly.CODEC); registry.register("enchant_with_levels", EnchantWithLevels.CODEC); registry.register("exploration_map", ExplorationMap.CODEC); registry.register("explosion_decay", ExplosionDecay.CODEC); registry.register("fill_player_head", FillPlayerHead.CODEC); registry.register("filtered", Filtered.CODEC); registry.register("furnace_smelt", FurnaceSmelt.CODEC); registry.register("limit_count", LimitCount.CODEC); registry.register("modify_contents", ModifyContents.CODEC); registry.register("reference", Reference.CODEC); registry.register("set_attributes", SetAttributes.CODEC); registry.register("set_banner_pattern", SetBannerPattern.CODEC); registry.register("set_book_cover", SetBookCover.CODEC); registry.register("set_components", SetComponents.CODEC); registry.register("set_contents", SetContents.CODEC); registry.register("set_count", SetCount.CODEC); registry.register("set_custom_data", SetCustomData.CODEC); registry.register("set_custom_model_data", SetCustomModelData.CODEC); registry.register("set_damage", SetDamage.CODEC); registry.register("set_enchantments", SetEnchantments.CODEC); registry.register("set_firework_explosion", SetFireworkExplosion.CODEC); registry.register("set_fireworks", SetFireworks.CODEC); registry.register("set_instrument", SetInstrument.CODEC); registry.register("set_item", SetItem.CODEC); registry.register("set_loot_table", SetLootTable.CODEC); registry.register("set_lore", SetLore.CODEC); registry.register("set_name", SetName.CODEC); registry.register("set_ominous_bottle_amplifier", SetOminousBottleAmplifier.CODEC); registry.register("set_potion", SetPotion.CODEC); registry.register("set_stew_effect", SetStewEffect.CODEC); registry.register("set_writable_book_pages", SetWritableBookPages.CODEC); registry.register("set_written_book_pages", SetWrittenBookPages.CODEC); registry.register("sequence", Sequence.CODEC); registry.register("toggle_tooltips", ToggleTooltips.CODEC); return registry; } /** * Performs any mutations on the provided object and returns the result. * @param input the input item to this function * @param context the context object, to use if required * @return the modified form of the input */ @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context); /** * @return the codec that can encode this function */ @NotNull StructCodec codec(); /** * Applies each function to the given item consecutively. * @param functions the functions to apply * @param item the item to modify * @param context the context to use * @return the modified item */ static @NotNull ItemStack apply(@NotNull Collection functions, @NotNull ItemStack item, @NotNull LootContext context) { for (LootFunction function : functions) { item = function.apply(item, context); } return item; } /** * Applies each function to each of the given items consecutively. * @param functions the functions to apply * @param items the items to modify * @param context the context to use * @return the modified items */ static @NotNull List apply(@NotNull Collection functions, @NotNull List items, @NotNull LootContext context) { List newItems = new ArrayList<>(items.size()); for (ItemStack item : items) { newItems.add(LootFunction.apply(functions, item, context)); } return newItems; } record ApplyBonus(@NotNull List predicates, @NotNull RegistryKey enchantment, @NotNull Formula.Wrapper formula) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), ApplyBonus::predicates, "enchantment", RegistryKey.codec(Registries::enchantment), ApplyBonus::enchantment, StructCodec.INLINE, Formula.CODEC, ApplyBonus::formula, ApplyBonus::new ); public sealed interface Formula { enum FormulaType { BINOMIAL_WITH_BONUS_COUNT, ORE_DROPS, UNIFORM_BONUS_COUNT, } record Wrapper(Formula parameters) { @SuppressWarnings("unchecked") private static StructCodec wrap(@NotNull StructCodec codec) { return StructCodec.struct( "parameters", codec.transform(a->a,a->(T)a), Wrapper::parameters, Wrapper::new ); } private static final StructCodec BINOMIAL_WITH_BONUS_COUNT = wrap(BinomialWithBonusCount.CODEC); private static final StructCodec ORE_DROPS = new StructCodec<>() { @Override public @NotNull Result decodeFromMap(@NotNull Transcoder coder, Transcoder.@NotNull MapLike map) { return new Result.Ok<>(new Wrapper(new OreDrops())); } @Override public @NotNull Result encodeToMap(@NotNull Transcoder coder, @NotNull Wrapper value, Transcoder.@NotNull MapBuilder map) { return new Result.Ok<>(map.build()); } }; private static final StructCodec UNIFORM_BONUS_COUNT = wrap(UniformBonusCount.CODEC); public static @NotNull StructCodec codec(@NotNull FormulaType type) { return switch (type) { case BINOMIAL_WITH_BONUS_COUNT -> BINOMIAL_WITH_BONUS_COUNT; case ORE_DROPS -> ORE_DROPS; case UNIFORM_BONUS_COUNT -> UNIFORM_BONUS_COUNT; }; } } @NotNull Codec CODEC = Codec.KEY.transform(key -> switch (key.asString()) { case "minecraft:binomial_with_bonus_count" -> FormulaType.BINOMIAL_WITH_BONUS_COUNT; case "minecraft:ore_drops" -> FormulaType.ORE_DROPS; case "minecraft:uniform_bonus_count" -> FormulaType.UNIFORM_BONUS_COUNT; default -> throw new IllegalArgumentException(); }, type -> Key.key(type.toString().toLowerCase())).unionType("formula", Wrapper::codec, (Wrapper formula) -> switch (formula.parameters()) { case BinomialWithBonusCount ignored -> FormulaType.BINOMIAL_WITH_BONUS_COUNT; case OreDrops ignored -> FormulaType.ORE_DROPS; case UniformBonusCount ignored -> FormulaType.UNIFORM_BONUS_COUNT; }); int calculate(@NotNull Random random, int count, int level); record UniformBonusCount(int bonusMultiplier) implements Formula { public static final @NotNull StructCodec CODEC = StructCodec.struct( "bonusMultiplier", StructCodec.INT, UniformBonusCount::bonusMultiplier, UniformBonusCount::new ); @Override public int calculate(@NotNull Random random, int count, int level) { return count + random.nextInt(bonusMultiplier * level + 1); } } record OreDrops() implements Formula { public static final @NotNull StructCodec CODEC = StructCodec.struct(OreDrops::new); @Override public int calculate(@NotNull Random random, int count, int level) { if (level <= 0) return count; return count * Math.max(1, random.nextInt(level + 2)); } } record BinomialWithBonusCount(float probability, int extra) implements Formula { public static final @NotNull StructCodec CODEC = StructCodec.struct( "probability", Codec.FLOAT, BinomialWithBonusCount::probability, "extra", Codec.INT, BinomialWithBonusCount::extra, BinomialWithBonusCount::new ); @Override public int calculate(@NotNull Random random, int count, int level) { for (int i = 0; i < extra + level; i++) { if (random.nextFloat() < probability) { count++; } } return count; } } } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { ItemStack tool = context.get(LootContext.TOOL); if (tool == null) return input; int level = EnchantmentUtils.level(tool, enchantment); int newCount = formula.parameters().calculate(context.require(LootContext.RANDOM), input.amount(), level); return input.withAmount(newCount); } @Override public @NotNull StructCodec codec() { return CODEC; } } record CopyComponents(@NotNull List predicates, @NotNull RelevantTarget source, @Nullable List> include, @Nullable List> exclude) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), CopyComponents::predicates, "source", RelevantTarget.CODEC, CopyComponents::source, "include", Codec.KEY.>transform(DataComponent::fromKey, DataComponent::key).list().optional(), CopyComponents::include, "exclude", Codec.KEY.>transform(DataComponent::fromKey, DataComponent::key).list().optional(), CopyComponents::exclude, CopyComponents::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; // TODO: Incomplete throw new UnsupportedOperationException("TODO: Implement Tag for blocks."); } @Override public @NotNull StructCodec codec() { return CODEC; } } record CopyCustomData(@NotNull List predicates, @NotNull LootNBT source, @NotNull List ops) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), CopyCustomData::predicates, "source", LootNBT.CODEC, CopyCustomData::source, "ops", Operation.CODEC.list(), CopyCustomData::ops, CopyCustomData::new ); public record Operation(@NotNull NBTPath source, @NotNull NBTPath target, @NotNull Operator op) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "source", NBTPath.CODEC, Operation::source, "target", NBTPath.CODEC, Operation::target, "op", Operator.SERIALIZER, Operation::op, Operation::new ); public void execute(@NotNull NBTReference nbt, @NotNull BinaryTag sourceTag) { List nbts = new ArrayList<>(); source.get(sourceTag).forEach(ref -> nbts.add(ref.get())); if (nbts.isEmpty()) return; op.merge(nbt, target, nbts); } } public enum Operator { REPLACE() { @Override public void merge(@NotNull NBTReference nbt, @NotNull NBTPath target, @NotNull List source) { target.set(nbt, source.getLast()); } }, APPEND() { @Override public void merge(@NotNull NBTReference nbt, @NotNull NBTPath target, @NotNull List source) { List nbts = target.getWithDefaults(nbt, ListBinaryTag::empty); for (var ref : nbts) { source.forEach(ref::listAdd); } } }, MERGE() { @Override public void merge(@NotNull NBTReference nbt, @NotNull NBTPath target, @NotNull List source) { List nbts = target.getWithDefaults(nbt, CompoundBinaryTag::empty); for (var ref : nbts) { if (ref.get() instanceof CompoundBinaryTag compound) { for (var nbt2 : source) { if (nbt2 instanceof CompoundBinaryTag compound2) { ref.set(NBTUtils.merge(compound, compound2)); } } } } } }; public static final @NotNull Codec SERIALIZER = Codec.Enum(Operator.class); public abstract void merge(@NotNull NBTReference nbt, @NotNull NBTPath target, @NotNull List source); } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; BinaryTag sourceNBT = source.getNBT(context); if (sourceNBT == null) return input; NBTReference targetNBT = NBTReference.of(input.get(DataComponents.CUSTOM_DATA, CustomData.EMPTY).nbt()); for (Operation operation : ops) { operation.execute(targetNBT, sourceNBT); } if (targetNBT.get() instanceof CompoundBinaryTag compound) { return input.with(DataComponents.CUSTOM_DATA, new CustomData(compound)); } else { return input; } } @Override public @NotNull StructCodec codec() { return CODEC; } } record CopyName(@NotNull List predicates, @NotNull RelevantTarget source) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), CopyName::predicates, "source", RelevantTarget.CODEC, CopyName::source, CopyName::new ); private static final Tag CUSTOM_NAME = Tag.Component("CustomName"); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; Object key = context.get(source.key()); Component customName; if (key instanceof Entity entity && entity.getCustomName() != null) { customName = entity.getCustomName(); } else if (key instanceof Block block && block.hasTag(CUSTOM_NAME)) { customName = block.getTag(CUSTOM_NAME); } else { return input; } return input.with(DataComponents.CUSTOM_NAME, customName); } @Override public @NotNull StructCodec codec() { return CODEC; } } record CopyState(@NotNull List predicates, @NotNull Block block, @NotNull List properties) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), CopyState::predicates, "block", Codec.KEY.transform(Block::fromKey, Block::key), CopyState::block, "properties", Codec.STRING.list(), CopyState::properties, CopyState::new ); public CopyState { List props = new ArrayList<>(properties); props.removeIf(name -> !block.properties().containsKey(name)); properties = List.copyOf(props); } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; Block block = context.get(LootContext.BLOCK_STATE); if (block == null) return input; ItemBlockState irritableBowelSyndrome = input.get(DataComponents.BLOCK_STATE, ItemBlockState.EMPTY); if (!block.key().equals(this.block.key())) return input; for (var prop : properties) { @Nullable String value = block.getProperty(prop); if (value == null) continue; irritableBowelSyndrome = irritableBowelSyndrome.with(prop, value); } return input.with(DataComponents.BLOCK_STATE, irritableBowelSyndrome); } @Override public @NotNull StructCodec codec() { return CODEC; } } record EnchantedCountIncrease(@NotNull List predicates, @NotNull RegistryKey enchantment, @NotNull LootNumber count, @Nullable Integer limit) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), EnchantedCountIncrease::predicates, "enchantment", RegistryKey.codec(Registries::enchantment), EnchantedCountIncrease::enchantment, "count", LootNumber.CODEC, EnchantedCountIncrease::count, "limit", Codec.INT.optional(), EnchantedCountIncrease::limit, EnchantedCountIncrease::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; Entity attacker = context.get(LootContext.ATTACKING_ENTITY); int level = EnchantmentUtils.level(attacker, enchantment); if (level == 0) return input; int newAmount = input.amount() + level * count.getInt(context); return input.withAmount(limit != null ? Math.min(limit, newAmount) : newAmount); } @Override public @NotNull StructCodec codec() { return CODEC; } } record EnchantRandomly(@NotNull List predicates, @Nullable RegistryTag options, boolean onlyCompatible) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), EnchantRandomly::predicates, "options", RegistryTag.codec(Registries::enchantment).optional(), EnchantRandomly::options, "only_compatible", Codec.BOOLEAN.optional(true), EnchantRandomly::onlyCompatible, EnchantRandomly::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { var reg = MinecraftServer.getEnchantmentRegistry(); List> values = new ArrayList<>(); (options == null ? reg.keys() : options).forEach(values::add); if (onlyCompatible && !input.material().equals(Material.BOOK)) { values.removeIf(ench -> !reg.get(ench).supportedItems().contains(input.material())); } if (values.isEmpty()) return input; Random rng = context.require(LootContext.RANDOM); RegistryKey chosen = values.get(rng.nextInt(values.size())); int level = rng.nextInt(reg.get(chosen).maxLevel() + 1); return EnchantmentUtils.modifyItem(input, map -> map.put(chosen, level)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record EnchantWithLevels(@NotNull List predicates, @NotNull LootNumber levels, @Nullable RegistryTag options) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), EnchantWithLevels::predicates, "levels", LootNumber.CODEC, EnchantWithLevels::levels, "options", RegistryTag.codec(Registries::enchantment).optional(), EnchantWithLevels::options, EnchantWithLevels::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (true) throw new UnsupportedOperationException("TODO: Implement enchanting (Random, ItemStack, int levels, @Nullable RegistryTag options -> ItemStack)"); return null; } @Override public @NotNull StructCodec codec() { return CODEC; } } record ExplorationMap(@NotNull List predicates) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), ExplorationMap::predicates, ExplorationMap::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; // TODO: Incomplete throw new UnsupportedOperationException("TODO: Implement ExplorationMap functionality and serialization"); } @Override public @NotNull StructCodec codec() { return CODEC; } } record ExplosionDecay(@NotNull List predicates) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), ExplosionDecay::predicates, ExplosionDecay::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; Float radius = context.get(LootContext.EXPLOSION_RADIUS); if (radius == null) return input; Random random = context.require(LootContext.RANDOM); float chance = 1 / radius; int trials = input.amount(); int newAmount = 0; for (int i = 0; i < trials; i++) { if (random.nextFloat() <= chance) { newAmount++; } } return input.withAmount(newAmount); } @Override public @NotNull StructCodec codec() { return CODEC; } } record FillPlayerHead(@NotNull List predicates, @NotNull RelevantEntity entity) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), FillPlayerHead::predicates, "entity", RelevantEntity.CODEC, FillPlayerHead::entity, FillPlayerHead::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (!input.material().equals(Material.PLAYER_HEAD)) return input; if (!(context.get(entity.key()) instanceof Player player)) return input; PlayerSkin skin = player.getSkin(); if (skin == null) return input; return input.with(DataComponents.PROFILE, new HeadProfile(skin)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Filtered(@NotNull List predicates, @NotNull ItemPredicate predicate, @NotNull LootFunction modifier) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Filtered::predicates, "item_filter", ItemPredicate.CODEC, Filtered::predicate, "modifier", LootFunction.CODEC, Filtered::modifier, Filtered::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { return LootPredicate.all(predicates, context) && predicate.test(input, context) ? modifier.apply(input, context) : input; } @Override public @NotNull StructCodec codec() { return CODEC; } } record FurnaceSmelt(@NotNull List predicates) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), FurnaceSmelt::predicates, FurnaceSmelt::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (true) throw new UnsupportedOperationException("TODO: Implement smelting (ItemStack -> ItemStack)"); ItemStack smelted = null; return smelted != null ? smelted.withAmount(input.amount()) : input; } @Override public @NotNull StructCodec codec() { return CODEC; } } record LimitCount(@NotNull List predicates, @NotNull LootNumberRange limit) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), LimitCount::predicates, "limit", LootNumberRange.CODEC, LimitCount::limit, LimitCount::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return input.withAmount(i -> (int) limit.limit(context, i)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record ModifyContents(@NotNull List predicates, @NotNull List modifier, @NotNull DataComponent> component) implements LootFunction { private static final @NotNull Map>> NAMED_CONTAINERS = Stream.of(DataComponents.CONTAINER, DataComponents.BUNDLE_CONTENTS, DataComponents.CHARGED_PROJECTILES) .collect(Collectors.toMap(DataComponent::key, Function.identity())); private static final @NotNull Codec>> CONTAINER = Codec.KEY.transform(NAMED_CONTAINERS::get, DataComponent::key); public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), ModifyContents::predicates, "modifier", LootFunction.CODEC.list(), ModifyContents::modifier, "component", CONTAINER, ModifyContents::component, ModifyContents::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; List items = input.get(component); if (items == null) return input; List updated = new ArrayList<>(); for (ItemStack item : items) { updated.add(LootFunction.apply(modifier, item, context)); } return input.with(component, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Reference(@NotNull List predicates, @NotNull Key name) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), Reference::predicates, "name", Codec.KEY, Reference::name, Reference::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (true) throw new UnsupportedOperationException("TODO: Implement loot function registry (Key -> @Nullable LootFunction)"); LootFunction function = null; return function != null ? function.apply(input, context) : input; } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetAttributes(@NotNull List predicates, @NotNull List modifiers, boolean replace) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetAttributes::predicates, "modifiers", AttributeDirective.CODEC.list(), SetAttributes::modifiers, "replace", Codec.BOOLEAN.optional(true), SetAttributes::replace, SetAttributes::new ); public record AttributeDirective(@NotNull Key id, @NotNull Attribute attribute, @NotNull AttributeOperation operation, @NotNull LootNumber amount, @NotNull List slots) { public static final @NotNull Codec CUSTOM_SLOT; static { Function name = slot -> slot.name().toLowerCase(Locale.ROOT).replace("_", ""); Map named = Stream.of(EquipmentSlot.values()).collect(Collectors.toMap(name, Function.identity())); CUSTOM_SLOT = Codec.STRING.transform(string -> Objects.requireNonNull(named.get(string)), name::apply); } public static final @NotNull Codec CODEC = StructCodec.struct( "id", Codec.KEY, AttributeDirective::id, "attribute", Attribute.CODEC, AttributeDirective::attribute, "operation", AttributeOperation.CODEC, AttributeDirective::operation, "amount", LootNumber.CODEC, AttributeDirective::amount, "slot", CUSTOM_SLOT.listOrSingle(Integer.MAX_VALUE), AttributeDirective::slots, AttributeDirective::new ); } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; var component = input.get(DataComponents.ATTRIBUTE_MODIFIERS, AttributeList.EMPTY); List list = replace ? new ArrayList<>() : new ArrayList<>(component.modifiers()); for (var modifier : modifiers) { if (modifier.slots().isEmpty()) continue; AttributeModifier mod = new AttributeModifier( modifier.id(), modifier.amount().getDouble(context), modifier.operation() ); EquipmentSlot slot = modifier.slots().get(context.require(LootContext.RANDOM).nextInt(modifier.slots().size())); EquipmentSlotGroup group = switch (slot) { case MAIN_HAND -> EquipmentSlotGroup.MAIN_HAND; case OFF_HAND -> EquipmentSlotGroup.OFF_HAND; case BOOTS -> EquipmentSlotGroup.FEET; case LEGGINGS -> EquipmentSlotGroup.LEGS; case CHESTPLATE -> EquipmentSlotGroup.CHEST; case HELMET -> EquipmentSlotGroup.HEAD; case BODY -> EquipmentSlotGroup.BODY; case SADDLE -> EquipmentSlotGroup.SADDLE; }; list.add(new AttributeList.Modifier(modifier.attribute(), mod, group)); } return input.with(DataComponents.ATTRIBUTE_MODIFIERS, new AttributeList(list)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetBannerPattern(@NotNull List predicates, @NotNull BannerPatterns patterns, boolean append) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetBannerPattern::predicates, "patterns", BannerPatterns.CODEC, SetBannerPattern::patterns, "append", Codec.BOOLEAN, SetBannerPattern::append, SetBannerPattern::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (append) { BannerPatterns patterns = input.get(DataComponents.BANNER_PATTERNS); if (patterns != null) { List layers = new ArrayList<>(patterns.layers()); layers.addAll(this.patterns().layers()); return input.with(DataComponents.BANNER_PATTERNS, new BannerPatterns(layers)); } } return input.with(DataComponents.BANNER_PATTERNS, patterns); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetBookCover(@NotNull List predicates, @Nullable FilteredText title, @Nullable String author, @Nullable Integer generation) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetBookCover::predicates, "title", FilteredText.STRING_CODEC.optional(), SetBookCover::title, "author", Codec.STRING.optional(), SetBookCover::author, "generation", Codec.INT.optional(), SetBookCover::generation, SetBookCover::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; WrittenBookContent content = input.get(DataComponents.WRITTEN_BOOK_CONTENT, WrittenBookContent.EMPTY); WrittenBookContent updated = new WrittenBookContent( title != null ? title : content.title(), author != null ? author : content.author(), generation != null ? generation : content.generation(), content.pages(), content.resolved() ); return input.with(DataComponents.WRITTEN_BOOK_CONTENT, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetComponents(@NotNull List predicates, @NotNull DataComponentMap changes) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetComponents::predicates, "components", DataComponent.PATCH_CODEC, SetComponents::changes, SetComponents::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; ItemStack.Builder builder = input.builder(); // This and .constantGeneric are hacks for until there exists a way to apply a patch to an item for (DataComponent.Value entry : changes.entrySet()) { constantGeneric(builder, entry.component(), entry.value()); } return builder.build(); } @SuppressWarnings("unchecked") private static void constantGeneric(@NotNull ItemStack.Builder builder, @NotNull DataComponent key, Object value) { builder.set(key, (T) value); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetContents(@NotNull List predicates, @NotNull List entries, @NotNull DataComponent> type) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetContents::predicates, "modifier", LootEntry.CODEC.list(), SetContents::entries, "type", ModifyContents.CONTAINER, SetContents::type, SetContents::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; List contents = new ArrayList<>(); for (LootEntry entry : entries) { for (LootEntry.Choice choice : entry.requestChoices(context)) { contents.addAll(choice.generate(context)); } } return input.with(type, contents); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetCount(@NotNull List predicates, @NotNull LootNumber count, boolean add) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetCount::predicates, "count", LootNumber.CODEC, SetCount::count, "add", Codec.BOOLEAN.optional(false), SetCount::add, SetCount::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return input.withAmount(amount -> (this.add ? amount : 0) + this.count.getInt(context)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetCustomData(@NotNull List predicates, @NotNull CompoundBinaryTag tag) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetCustomData::predicates, "tag", Codec.NBT_COMPOUND, SetCustomData::tag, SetCustomData::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return input.with(DataComponents.CUSTOM_DATA, new CustomData(tag)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetCustomModelData(@NotNull List predicates, @NotNull LootNumber value) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetCustomModelData::predicates, "value", LootNumber.CODEC, SetCustomModelData::value, SetCustomModelData::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return input; // TODO: Incomplete // return input.with(ItemComponent.CUSTOM_MODEL_DATA, new CustomModelData(value.getInt(context)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetDamage(@NotNull List predicates, @NotNull LootNumber damage, boolean add) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetDamage::predicates, "damage", LootNumber.CODEC, SetDamage::damage, "add", Codec.BOOLEAN.optional(false), SetDamage::add, SetDamage::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; int maxDamage = input.get(DataComponents.MAX_DAMAGE, -1); if (maxDamage == -1) return input; double damage = input.get(DataComponents.DAMAGE, 0) / (double) maxDamage; double currentDura = add ? 1 - damage : 0; double newDura = Math.max(0, Math.min(1, currentDura + this.damage.getDouble(context))); double newDamage = 1 - newDura; return input.with(DataComponents.DAMAGE, (int) Math.floor(newDamage * maxDamage)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetEnchantments(@NotNull List predicates, @NotNull Map, LootNumber> enchantments, boolean add) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetEnchantments::predicates, "enchantments", RegistryKey.codec(Registries::enchantment).mapValue(LootNumber.CODEC), SetEnchantments::enchantments, "add", Codec.BOOLEAN.optional(false), SetEnchantments::add, SetEnchantments::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return EnchantmentUtils.modifyItem(input, map -> { this.enchantments.forEach((enchantment, number) -> { int count = number.getInt(context); if (add) { count += map.getOrDefault(enchantment, 0); } map.put(enchantment, count); }); }); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetFireworkExplosion(@NotNull List predicates, @Nullable FireworkExplosion.Shape shape, @Nullable List colors, @Nullable List fadeColors, @Nullable Boolean trail, @Nullable Boolean twinkle) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetFireworkExplosion::predicates, "shape", Codec.Enum(FireworkExplosion.Shape.class).optional(), SetFireworkExplosion::shape, "colors", Color.CODEC.list().optional(), SetFireworkExplosion::colors, "fade_colors", Color.CODEC.list().optional(), SetFireworkExplosion::fadeColors, "has_trail", Codec.BOOLEAN.optional(), SetFireworkExplosion::trail, "has_twinkle", Codec.BOOLEAN.optional(), SetFireworkExplosion::twinkle, SetFireworkExplosion::new ); private static final @NotNull FireworkExplosion DEFAULT = new FireworkExplosion(FireworkExplosion.Shape.SMALL_BALL, List.of(), List.of(), false, false); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; FireworkExplosion firework = input.get(DataComponents.FIREWORK_EXPLOSION, DEFAULT); FireworkExplosion updated = new FireworkExplosion( this.shape != null ? this.shape : firework.shape(), this.colors != null ? this.colors : firework.colors(), this.fadeColors != null ? this.fadeColors : firework.fadeColors(), this.trail != null ? this.trail : firework.hasTrail(), this.twinkle != null ? this.twinkle : firework.hasTwinkle() ); return input.with(DataComponents.FIREWORK_EXPLOSION, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetFireworks(@NotNull List predicates, @Nullable Integer flightDuration, @NotNull ListOperation.WithValues explosions) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetFireworks::predicates, "flight_duration", Codec.INT.optional(), SetFireworks::flightDuration, "explosions", ListOperation.WithValues.codec(FireworkExplosion.CODEC), SetFireworks::explosions, SetFireworks::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; FireworkList list = input.get(DataComponents.FIREWORKS, FireworkList.EMPTY); FireworkList updated = new FireworkList( this.flightDuration != null ? this.flightDuration.byteValue() : list.flightDuration(), explosions.operation().apply(this.explosions.values(), list.explosions()) ); return input.with(DataComponents.FIREWORKS, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetInstrument(@NotNull List predicates, @NotNull RegistryTag options) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetInstrument::predicates, "options", RegistryTag.codec(Registries::instrument), SetInstrument::options, SetInstrument::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (options.size() == 0) return input; int count = context.require(LootContext.RANDOM).nextInt(options.size()); var it = options.iterator(); for (int i = 0; i < count; i++) { it.next(); } return input.with(DataComponents.INSTRUMENT, new InstrumentComponent(Either.right(it.next()))); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetItem(@NotNull List predicates, @NotNull Material item) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetItem::predicates, "item", Material.CODEC, SetItem::item, SetItem::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; return input.builder().material(item).build(); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetLootTable(@NotNull List predicates, @NotNull Key name, long seed) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetLootTable::predicates, "name", Codec.KEY, SetLootTable::name, "seed", Codec.LONG.optional(0L), SetLootTable::seed, SetLootTable::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (input.isAir()) return input; return input.with(DataComponents.CONTAINER_LOOT, new SeededContainerLoot(name.asString(), seed)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetLore(@NotNull List predicates, @NotNull List lore, @NotNull ListOperation operation, @Nullable RelevantEntity entity) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetLore::predicates, "lore", Codec.COMPONENT.list(), SetLore::lore, StructCodec.INLINE, ListOperation.CODEC, SetLore::operation, "entity", RelevantEntity.CODEC.optional(), SetLore::entity, SetLore::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; List components = input.get(DataComponents.LORE, List.of()); // TODO: Incomplete // TODO: https://minecraft.wiki/w/Raw_JSON_text_format#Component_resolution // This is not used in vanilla so it's fine for now. return input.with(DataComponents.LORE, operation.apply(lore, components)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetName(@NotNull List predicates, @Nullable Component name, @Nullable RelevantEntity entity, @NotNull Target target) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetName::predicates, "name", Codec.COMPONENT.optional(), SetName::name, "entity", RelevantEntity.CODEC.optional(), SetName::entity, "target", Target.CODEC.optional(Target.CUSTOM_NAME), SetName::target, SetName::new ); public enum Target { ITEM_NAME("item_name", DataComponents.ITEM_NAME), CUSTOM_NAME("custom_name", DataComponents.CUSTOM_NAME); public static final @NotNull Codec CODEC = Codec.Enum(Target.class); // Relies on the enum names themselves being accurate private final String id; private final DataComponent component; Target(String id, DataComponent component) { this.id = id; this.component = component; } public String id() { return id; } public DataComponent component() { return component; } } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (name == null) return input; Component component = this.name; // TODO: Incomplete // TODO: https://minecraft.wiki/w/Raw_JSON_text_format#Component_resolution // This is not used in vanilla so it's fine for now. return input.with(target.component(), component); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetOminousBottleAmplifier(@NotNull List predicates, @NotNull LootNumber amplifier) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetOminousBottleAmplifier::predicates, "amplifier", LootNumber.CODEC, SetOminousBottleAmplifier::amplifier, SetOminousBottleAmplifier::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; int amplifier = Math.max(0, Math.min(this.amplifier.getInt(context), 4)); return input.with(DataComponents.OMINOUS_BOTTLE_AMPLIFIER, amplifier); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetPotion(@NotNull List predicates, @NotNull Key id) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetPotion::predicates, "id", Codec.KEY, SetPotion::id, SetPotion::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (id.asString().equals("minecraft:empty")) { return input.without(DataComponents.POTION_CONTENTS); } PotionContents existing = input.get(DataComponents.POTION_CONTENTS, PotionContents.EMPTY); PotionContents updated = new PotionContents(PotionType.fromKey(id), existing.customColor(), existing.customEffects()); return input.with(DataComponents.POTION_CONTENTS, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetStewEffect(@NotNull List predicates, @NotNull List effects) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetStewEffect::predicates, "effects", AddedEffect.CODEC.list(), SetStewEffect::effects, SetStewEffect::new ); public record AddedEffect(@NotNull PotionEffect effect, @NotNull LootNumber duration) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "type", PotionEffect.CODEC, AddedEffect::effect, "duration", LootNumber.CODEC, AddedEffect::duration, AddedEffect::new ); } @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; if (!Material.SUSPICIOUS_STEW.equals(input.material()) || effects.isEmpty()) return input; AddedEffect effect = effects.get(context.require(LootContext.RANDOM).nextInt(effects.size())); long duration = effect.duration().getInt(context); if (!effect.effect().registry().isInstantaneous()) { duration *= ServerFlag.SERVER_TICKS_PER_SECOND; } SuspiciousStewEffects.Effect added = new SuspiciousStewEffects.Effect(effect.effect(), (int) duration); SuspiciousStewEffects current = input.get(DataComponents.SUSPICIOUS_STEW_EFFECTS, SuspiciousStewEffects.EMPTY); return input.with(DataComponents.SUSPICIOUS_STEW_EFFECTS, current.with(added)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetWritableBookPages(@NotNull List predicates, @NotNull List> pages, @NotNull ListOperation operation) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetWritableBookPages::predicates, "pages", FilteredText.STRING_CODEC.list(), SetWritableBookPages::pages, StructCodec.INLINE, ListOperation.CODEC, SetWritableBookPages::operation, SetWritableBookPages::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; WritableBookContent content = input.get(DataComponents.WRITABLE_BOOK_CONTENT, WritableBookContent.EMPTY); return input.with(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(operation.apply(pages, content.pages()))); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SetWrittenBookPages(@NotNull List predicates, @NotNull List> pages, @NotNull ListOperation operation) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), SetWrittenBookPages::predicates, "pages", FilteredText.COMPONENT_CODEC.list(), SetWrittenBookPages::pages, StructCodec.INLINE, ListOperation.CODEC, SetWrittenBookPages::operation, SetWrittenBookPages::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; WrittenBookContent content = input.get(DataComponents.WRITTEN_BOOK_CONTENT, WrittenBookContent.EMPTY); WrittenBookContent updated = new WrittenBookContent(content.title(), content.author(), content.generation(), operation.apply(pages, content.pages()), content.resolved()); return input.with(DataComponents.WRITTEN_BOOK_CONTENT, updated); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Sequence(@NotNull List functions) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "functions", LootFunction.CODEC.list(), Sequence::functions, Sequence::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { return LootFunction.apply(functions, input, context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record ToggleTooltips(@NotNull List predicates, @NotNull Map, Boolean> toggles) implements LootFunction { public static final @NotNull StructCodec CODEC = StructCodec.struct( "conditions", LootPredicate.CODEC.list().optional(List.of()), ToggleTooltips::predicates, "toggles", DataComponent.CODEC.mapValue(StructCodec.BOOLEAN), ToggleTooltips::toggles, ToggleTooltips::new ); @Override public @NotNull ItemStack apply(@NotNull ItemStack input, @NotNull LootContext context) { if (!LootPredicate.all(predicates, context)) return input; TooltipDisplay display = input.get(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.EMPTY); for (var entry : toggles.entrySet()) { display = entry.getValue() ? display.with(entry.getKey()) : display.without(entry.getKey()); } return input.with(DataComponents.TOOLTIP_DISPLAY, display); } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootGenerator.java ================================================ package net.minestom.vanilla.loot; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import java.util.List; /** * Something that can generate loot. */ public interface LootGenerator { @NotNull List generate(@NotNull LootContext context); } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootNBT.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.IntBinaryTag; import net.kyori.adventure.nbt.StringBinaryTag; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.block.Block; import net.minestom.server.registry.DynamicRegistry; import net.minestom.vanilla.loot.util.RelevantEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; /** * Returns NBT data from the provided context. */ @SuppressWarnings("UnstableApiUsage") public interface LootNBT { @NotNull Codec CODEC = Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootNBT::codec, "type").orElse(Codec.STRING.transform( str -> new Context(Context.Target.fromString(str)), nbt -> ((Context) nbt).target.toString() )); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_nbt")); registry.register("context", Context.CODEC); registry.register("storage", Storage.CODEC); return registry; } /** * Generates some NBT based on the provided context. * @param context the context to use for NBT * @return the NBT, or null if there is none */ @Nullable BinaryTag getNBT(@NotNull LootContext context); /** * @return the codec that can encode this function */ @NotNull StructCodec codec(); record Storage(@NotNull Key source) implements LootNBT { public static final @NotNull StructCodec CODEC = StructCodec.struct( "source", Codec.KEY, Storage::source, Storage::new ); @Override public @Nullable BinaryTag getNBT(@NotNull LootContext context) { if (true) throw new UnsupportedOperationException("TODO: Implement command storage (Key -> CompoundBinaryTag)"); return null; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Context(@NotNull Target target) implements LootNBT { public static final @NotNull StructCodec CODEC = StructCodec.struct( "target", Codec.STRING.transform(Context.Target::fromString, Context.Target::toString), Context::target, Context::new ); public sealed interface Target { @Nullable BinaryTag getNBT(@NotNull LootContext context); static @NotNull Target fromString(@NotNull String input) { if (input.equals("block_entity")) return new BlockEntity(); for (var target : RelevantEntity.values()) { if (target.id().equals(input)) return new Entity(target); } throw new IllegalArgumentException("Expected block_entity or a valid entity target name"); } record BlockEntity() implements Target { @SuppressWarnings("DataFlowIssue") @Override public @NotNull BinaryTag getNBT(@NotNull LootContext context) { Block block = context.require(LootContext.BLOCK_STATE); Point pos = context.require(LootContext.ORIGIN); CompoundBinaryTag nbt = block.hasNbt() ? block.nbt() : CompoundBinaryTag.empty(); return nbt.put(Map.of( "x", IntBinaryTag.intBinaryTag(pos.blockX()), "y", IntBinaryTag.intBinaryTag(pos.blockY()), "z", IntBinaryTag.intBinaryTag(pos.blockZ()), "id", StringBinaryTag.stringBinaryTag(block.key().asString()) )); } @Override public String toString() { return "block_entity"; } } record Entity(@NotNull RelevantEntity target) implements Target { @Override public @NotNull BinaryTag getNBT(@NotNull LootContext context) { var entity = context.require(target.key()); if (true) throw new UnsupportedOperationException("TODO: Implement entity serialization (Entity entity -> BinaryTag)"); return null; } @Override public String toString() { return target.id(); } } } @Override public @Nullable BinaryTag getNBT(@NotNull LootContext context) { return target.getNBT(context); } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootNumber.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.IntBinaryTag; import net.kyori.adventure.nbt.NumberBinaryTag; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.item.enchant.LevelBasedValue; import net.minestom.server.registry.DynamicRegistry; import net.minestom.vanilla.loot.util.nbt.NBTPath; import net.minestom.vanilla.loot.util.nbt.NBTReference; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Random; /** * Generates numbers based on provided loot contexts. */ @SuppressWarnings("UnstableApiUsage") public interface LootNumber { @NotNull Codec CODEC = Codec.DOUBLE.transform(Constant::new, a -> ((Constant) a).value()).orElse(Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootNumber::codec, "type")); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_numbers")); registry.register("binomial", Binomial.CODEC); registry.register("constant", Constant.CODEC); registry.register("enchantment_level", EnchantmentLevel.CODEC); registry.register("score", Score.CODEC); registry.register("storage", Storage.CODEC); registry.register("uniform", Uniform.CODEC); return registry; } /** * Generates an integer depending on the information in the provided context.
* This is an explicitly impure method—it depends on state outside the given context. * @param context the context object, to use if required * @return the integer generated by this loot number for the provided context */ int getInt(@NotNull LootContext context); /** * Generates a double depending on the information in the provided context.
* This is an explicitly impure method—it depends on state outside the given context. * @param context the context object, to use if required * @return the double generated by this loot number for the provided context */ double getDouble(@NotNull LootContext context); /** * @return the codec that can encode this number */ @NotNull StructCodec codec(); record Binomial(@NotNull LootNumber trials, @NotNull LootNumber probability) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "n", LootNumber.CODEC, Binomial::trials, "p", LootNumber.CODEC, Binomial::probability, Binomial::new ); @Override public int getInt(@NotNull LootContext context) { int trials = trials().getInt(context); double probability = probability().getDouble(context); Random random = context.require(LootContext.RANDOM); int successes = 0; for (int trial = 0; trial < trials; trial++) { if (random.nextDouble() < probability) { successes++; } } return successes; } @Override public double getDouble(@NotNull LootContext context) { return getInt(context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Constant(@NotNull Double value) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "value", Codec.DOUBLE, Constant::value, Constant::new ); @Override public int getInt(@NotNull LootContext context) { return value.intValue(); } @Override public double getDouble(@NotNull LootContext context) { return value; } @Override public @NotNull StructCodec codec() { return CODEC; } } record EnchantmentLevel(@NotNull LevelBasedValue amount) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "amount", LevelBasedValue.CODEC, EnchantmentLevel::amount, EnchantmentLevel::new ); @Override public int getInt(@NotNull LootContext context) { return (int) Math.round(getDouble(context)); } @Override public double getDouble(@NotNull LootContext context) { return amount.calc(context.require(LootContext.ENCHANTMENT_LEVEL)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Score(@NotNull LootScore target, @NotNull String objective, double scale) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "target", LootScore.CODEC, Score::target, "score", Codec.STRING, Score::objective, "scale", Codec.DOUBLE, Score::scale, Score::new ); @Override public int getInt(@NotNull LootContext context) { return (int) Math.round(getDouble(context)); } @Override public double getDouble(@NotNull LootContext context) { var score = target.apply(context).apply(objective); return score != null ? score * scale : 0; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Storage(@NotNull Key storage, @NotNull NBTPath path) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "storage", Codec.KEY, Storage::storage, "path", NBTPath.CODEC, Storage::path, Storage::new ); @Override public int getInt(@NotNull LootContext context) { return get(context).intValue(); } @Override public double getDouble(@NotNull LootContext context) { return get(context).doubleValue(); } private NumberBinaryTag get(@NotNull LootContext context) { if (true) throw new UnsupportedOperationException("TODO: Implement entity scores (Entity entity -> String objective -> @Nullable Integer)"); CompoundBinaryTag compound = null; List refs = path.get(compound != null ? compound : CompoundBinaryTag.empty()); if (refs.size() != 1) return IntBinaryTag.intBinaryTag(0); if (refs.getFirst().get() instanceof NumberBinaryTag number) { return number; } else { return IntBinaryTag.intBinaryTag(0); } } @Override public @NotNull StructCodec codec() { return CODEC; } } record Uniform(@NotNull LootNumber min, @NotNull LootNumber max) implements LootNumber { public static final @NotNull StructCodec CODEC = StructCodec.struct( "min", LootNumber.CODEC, Uniform::min, "max", LootNumber.CODEC, Uniform::max, Uniform::new ); @Override public int getInt(@NotNull LootContext context) { return context.require(LootContext.RANDOM).nextInt(min().getInt(context), max().getInt(context) + 1); } @Override public double getDouble(@NotNull LootContext context) { return context.require(LootContext.RANDOM).nextDouble(min().getDouble(context), max().getDouble(context)); } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootPool.java ================================================ package net.minestom.vanilla.loot; import net.minestom.server.codec.StructCodec; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; /** * A loot pool. * @param rolls the default number of rolls to occur * @param bonusRolls the number of bonus rolls to occur. Multiplied by the context's luck. * @param entries the entries to generate loot from. * @param predicates the predicates for loot generation * @param functions the modifiers applied to each item */ public record LootPool(@NotNull LootNumber rolls, @NotNull LootNumber bonusRolls, @NotNull List entries, @NotNull List predicates, @NotNull List functions) implements LootGenerator { @SuppressWarnings("UnstableApiUsage") public static final @NotNull StructCodec CODEC = StructCodec.struct( "rolls", LootNumber.CODEC, LootPool::rolls, "bonus_rolls", LootNumber.CODEC.optional(new LootNumber.Constant(0D)), LootPool::bonusRolls, "entries", LootEntry.CODEC.list(), LootPool::entries, "conditions", LootPredicate.CODEC.list().optional(List.of()), LootPool::predicates, "functions", LootFunction.CODEC.list().optional(List.of()), LootPool::functions, LootPool::new ); @Override public @NotNull List generate(@NotNull LootContext context) { if (!(LootPredicate.all(predicates, context))) return List.of(); int rolls = this.rolls.getInt(context); Double luck = context.get(LootContext.LUCK); if (luck != null) { rolls += (int) Math.floor(luck * this.bonusRolls.getDouble(context)); } List items = new ArrayList<>(); for (int i = 0; i < rolls; i++) { LootEntry.Choice choice = pickChoice(entries, context); if (choice == null) continue; items.addAll(choice.generate(context)); } return LootFunction.apply(functions, items, context); } /** * Picks a random choice from the choices generated by the provided entries, weighted with each choice's weight. If * no choices were generated, null is returned. * @param entries the entries to generate choices to choose from * @param context the context, to use if needed * @return the picked choice, or null if no choices were generated */ static @Nullable LootEntry.Choice pickChoice(@NotNull List entries, @NotNull LootContext context) { List choices = new ArrayList<>(); for (LootEntry entry : entries) { choices.addAll(entry.requestChoices(context)); } if (choices.isEmpty()) { return null; } long totalWeight = 0; long[] weightMilestones = new long[choices.size()]; for (int i = 0; i < choices.size(); i++) { // Prevent the weight of this choice from being less than 1 totalWeight += Math.max(1, choices.get(i).getWeight(context)); weightMilestones[i] = totalWeight; } long value = context.require(LootContext.RANDOM).nextLong(0, totalWeight); LootEntry.Choice choice = choices.getLast(); for (int i = 0; i < weightMilestones.length; i++) { if (value < weightMilestones[i]) { choice = choices.get(i); break; } } return choice; } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootPredicate.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.Weather; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.predicate.PropertiesPredicate; import net.minestom.server.item.ItemStack; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.item.enchant.LevelBasedValue; import net.minestom.server.registry.DynamicRegistry; import net.minestom.server.registry.Registries; import net.minestom.server.registry.RegistryKey; import net.minestom.vanilla.loot.util.EnchantmentUtils; import net.minestom.vanilla.loot.util.LootNumberRange; import net.minestom.vanilla.loot.util.RelevantEntity; import net.minestom.vanilla.loot.util.predicate.DamageSourcePredicate; import net.minestom.vanilla.loot.util.predicate.EntityPredicate; import net.minestom.vanilla.loot.util.predicate.ItemPredicate; import net.minestom.vanilla.loot.util.predicate.LocationPredicate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Map; import java.util.function.Predicate; /** * A predicate over a loot context, returning whether or not a given context passes some arbitrary predicate. */ @SuppressWarnings("UnstableApiUsage") public interface LootPredicate extends Predicate<@NotNull LootContext> { @NotNull StructCodec CODEC = Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootPredicate::codec, "condition"); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_predicates")); registry.register("all_of", AllOf.CODEC); registry.register("any_of", AnyOf.CODEC); registry.register("block_state_property", BlockStateProperty.CODEC); registry.register("damage_source_properties", DamageSourceProperties.CODEC); registry.register("enchantment_active_check", EnchantmentActiveCheck.CODEC); registry.register("entity_properties", EntityProperties.CODEC); registry.register("entity_scores", EntityScores.CODEC); registry.register("inverted", Inverted.CODEC); registry.register("killed_by_player", KilledByPlayer.CODEC); registry.register("location_check", LocationCheck.CODEC); registry.register("match_tool", MatchTool.CODEC); registry.register("random_chance", RandomChance.CODEC); registry.register("random_chance_with_enchanted_bonus", RandomChanceWithEnchantedBonus.CODEC); registry.register("reference", Reference.CODEC); registry.register("survives_explosion", SurvivesExplosion.CODEC); registry.register("table_bonus", TableBonus.CODEC); registry.register("time_check", TimeCheck.CODEC); registry.register("value_check", ValueCheck.CODEC); registry.register("weather_check", WeatherCheck.CODEC); return registry; } /** * Returns whether or not the provided loot context passes this predicate. * @param context the context object, to use if required * @return true if the provided loot context is valid according to this predicate */ @Override boolean test(@NotNull LootContext context); /** * @return the codec that can encode this predicate */ @NotNull StructCodec codec(); /** * Returns whether or not every given predicate verifies the provided context. */ static boolean all(@NotNull List predicates, @NotNull LootContext context) { if (predicates.isEmpty()) { return true; } for (var predicate : predicates) { if (!predicate.test(context)) { return false; } } return true; } record AllOf(@NotNull List terms) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "terms", LootPredicate.CODEC.list(), AllOf::terms, AllOf::new ); @Override public boolean test(@NotNull LootContext context) { return all(terms, context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record AnyOf(@NotNull List terms) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "terms", LootPredicate.CODEC.list(), AnyOf::terms, AnyOf::new ); @Override public boolean test(@NotNull LootContext context) { if (terms.isEmpty()) { return false; } for (var predicate : terms) { if (predicate.test(context)) { return true; } } return false; } @Override public @NotNull StructCodec codec() { return CODEC; } } record BlockStateProperty(@NotNull Key block, @Nullable PropertiesPredicate properties) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "block", Codec.KEY, BlockStateProperty::block, "properties", PropertiesPredicate.CODEC.optional(), BlockStateProperty::properties, BlockStateProperty::new ); @Override public boolean test(@NotNull LootContext context) { Block block = context.get(LootContext.BLOCK_STATE); return block != null && this.block.equals(block.key()) && (properties == null || properties.test(block)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record DamageSourceProperties(@Nullable DamageSourcePredicate predicate) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "predicate", DamageSourcePredicate.CODEC.optional(), DamageSourceProperties::predicate, DamageSourceProperties::new ); @Override public boolean test(@NotNull LootContext context) { Instance world = context.get(LootContext.WORLD); Point origin = context.get(LootContext.ORIGIN); DamageType damage = context.get(LootContext.DAMAGE_SOURCE); if (predicate == null || world == null || origin == null || damage == null) { return false; } return predicate.test(world, origin, damage); } @Override public @NotNull StructCodec codec() { return CODEC; } } record EnchantmentActiveCheck(boolean active) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "active", Codec.BOOLEAN, EnchantmentActiveCheck::active, EnchantmentActiveCheck::new ); @Override public boolean test(@NotNull LootContext context) { return context.require(LootContext.ENCHANTMENT_ACTIVE) == active; } @Override public @NotNull StructCodec codec() { return CODEC; } } record EntityProperties(@Nullable EntityPredicate predicate, @NotNull RelevantEntity entity) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "predicate", EntityPredicate.CODEC, EntityProperties::predicate, "entity", RelevantEntity.CODEC, EntityProperties::entity, EntityProperties::new ); @Override public boolean test(@NotNull LootContext context) { Entity entity = context.get(this.entity.key()); Point origin = context.get(LootContext.ORIGIN); return predicate == null || predicate.test(context.require(LootContext.WORLD), origin, entity); } @Override public @NotNull StructCodec codec() { return CODEC; } } record EntityScores(@NotNull Map scores, @NotNull RelevantEntity entity) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "scores", Codec.STRING.mapValue(LootNumberRange.CODEC), EntityScores::scores, "entity", RelevantEntity.CODEC, EntityScores::entity, EntityScores::new ); @Override public boolean test(@NotNull LootContext context) { Entity entity = context.get(this.entity.key()); if (entity == null) return false; for (var entry : scores.entrySet()) { if (true) throw new UnsupportedOperationException("TODO: Implement entity scores (Entity entity -> String objective -> @Nullable Integer)"); Integer score = null; if (score == null || !entry.getValue().check(context, score)) { return false; } } return true; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Inverted(@NotNull LootPredicate term) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "term", LootPredicate.CODEC, Inverted::term, Inverted::new ); @Override public boolean test(@NotNull LootContext context) { return !term.test(context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record KilledByPlayer() implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct(KilledByPlayer::new); @Override public boolean test(@NotNull LootContext context) { return context.has(LootContext.LAST_DAMAGE_PLAYER); } @Override public @NotNull StructCodec codec() { return CODEC; } } record LocationCheck(@Nullable LocationPredicate predicate, double offsetX, double offsetY, double offsetZ) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "predicate", LocationPredicate.CODEC, LocationCheck::predicate, "offsetX", Codec.DOUBLE.optional(0D), LocationCheck::offsetX, "offsetY", Codec.DOUBLE.optional(0D), LocationCheck::offsetY, "offsetZ", Codec.DOUBLE.optional(0D), LocationCheck::offsetZ, LocationCheck::new ); @Override public boolean test(@NotNull LootContext context) { Point origin = context.get(LootContext.ORIGIN); if (origin == null) return false; if (predicate == null) return true; return predicate.test(context.require(LootContext.WORLD), origin.add(offsetX, offsetY, offsetZ)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record MatchTool(@Nullable ItemPredicate predicate) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "predicate", ItemPredicate.CODEC, MatchTool::predicate, MatchTool::new ); @Override public boolean test(@NotNull LootContext context) { ItemStack tool = context.get(LootContext.TOOL); if (tool == null) return false; if (predicate == null) return true; return predicate.test(tool, context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record RandomChance(@NotNull LootNumber chance) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "chance", LootNumber.CODEC, RandomChance::chance, RandomChance::new ); @Override public boolean test(@NotNull LootContext context) { return context.require(LootContext.RANDOM).nextDouble() < chance.getDouble(context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record RandomChanceWithEnchantedBonus(@NotNull RegistryKey enchantment, float unenchantedChance, @NotNull LevelBasedValue enchantedChance) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "enchantment", RegistryKey.codec(Registries::enchantment), RandomChanceWithEnchantedBonus::enchantment, "unenchanted_chance", Codec.FLOAT, RandomChanceWithEnchantedBonus::unenchantedChance, "enchanted_chance", LevelBasedValue.CODEC, RandomChanceWithEnchantedBonus::enchantedChance, RandomChanceWithEnchantedBonus::new ); @Override public boolean test(@NotNull LootContext context) { Entity attacker = context.get(LootContext.ATTACKING_ENTITY); int level = EnchantmentUtils.level(attacker, enchantment); float chance = level > 0 ? enchantedChance.calc(level) : unenchantedChance; return context.require(LootContext.RANDOM).nextFloat() < chance; } @Override public @NotNull StructCodec codec() { return CODEC; } } record Reference(@NotNull Key name) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "name", Codec.KEY, Reference::name, Reference::new ); @Override public boolean test(@NotNull LootContext context) { if (true) throw new UnsupportedOperationException("TODO: Implement loot predicate registry (Key -> @Nullable LootPredicate)"); LootPredicate predicate = null; return predicate != null && predicate.test(context); } @Override public @NotNull StructCodec codec() { return CODEC; } } record SurvivesExplosion() implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct(SurvivesExplosion::new); @Override public boolean test(@NotNull LootContext context) { Float radius = context.get(LootContext.EXPLOSION_RADIUS); return radius == null || context.require(LootContext.RANDOM).nextFloat() <= (1 / radius); } @Override public @NotNull StructCodec codec() { return CODEC; } } record TableBonus(@NotNull RegistryKey enchantment, @NotNull List chances) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "enchantment", RegistryKey.codec(Registries::enchantment), TableBonus::enchantment, "chances", Codec.FLOAT.list(), TableBonus::chances, TableBonus::new ); @Override public boolean test(@NotNull LootContext context) { ItemStack tool = context.get(LootContext.TOOL); int level = EnchantmentUtils.level(tool, enchantment); float chance = chances.get(Math.min(this.chances.size() - 1, level)); return context.require(LootContext.RANDOM).nextFloat() < chance; } @Override public @NotNull StructCodec codec() { return CODEC; } } record TimeCheck(@Nullable Long period, @NotNull LootNumberRange value) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "period", Codec.LONG.optional(), TimeCheck::period, "value", LootNumberRange.CODEC, TimeCheck::value, TimeCheck::new ); @Override public boolean test(@NotNull LootContext context) { long time = context.require(LootContext.WORLD).getTime(); if (period != null) { time %= period; } return value.check(context, time); } @Override public @NotNull StructCodec codec() { return CODEC; } } record ValueCheck(@NotNull LootNumber value, @NotNull LootNumberRange range) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "value", LootNumber.CODEC, ValueCheck::value, "range", LootNumberRange.CODEC, ValueCheck::range, ValueCheck::new ); @Override public boolean test(@NotNull LootContext context) { return range.check(context, value.getInt(context)); } @Override public @NotNull StructCodec codec() { return CODEC; } } record WeatherCheck(@Nullable Boolean raining, @Nullable Boolean thundering) implements LootPredicate { public static final @NotNull StructCodec CODEC = StructCodec.struct( "raining", Codec.BOOLEAN.optional(), WeatherCheck::raining, "thundering", Codec.BOOLEAN.optional(), WeatherCheck::thundering, WeatherCheck::new ); @Override public boolean test(@NotNull LootContext context) { Weather weather = context.require(LootContext.WORLD).getWeather(); return (raining == null || raining == weather.isRaining()) && (thundering == null || thundering == weather.thunderLevel() > 0); } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootScore.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.registry.DynamicRegistry; import net.minestom.vanilla.loot.util.RelevantEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Function; /** * A score provider that produces functions that map an objective to a score value. */ @SuppressWarnings("UnstableApiUsage") public interface LootScore extends Function<@NotNull LootContext, Function<@NotNull String, @Nullable Integer>> { @NotNull Codec CODEC = RelevantEntity.CODEC.transform(Context::new, c -> ((Context) c).name()).orElse(Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, LootScore::codec, "type")); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("loot_scores")); registry.register("context", Context.CODEC); registry.register("fixed", Fixed.CODEC); return registry; } @Override @NotNull Function<@NotNull String, @Nullable Integer> apply(@NotNull LootContext context); /** * @return the codec that can encode this score */ @NotNull StructCodec codec(); record Context(@NotNull RelevantEntity name) implements LootScore { public static final @NotNull StructCodec CODEC = StructCodec.struct( "name", RelevantEntity.CODEC, Context::name, Context::new ); @Override public @NotNull Function<@NotNull String, @Nullable Integer> apply(@NotNull LootContext context) { throw new UnsupportedOperationException("TODO: Implement entity scores (Entity entity -> String objective -> @Nullable Integer)"); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Fixed(@NotNull String name) implements LootScore { public static final @NotNull StructCodec CODEC = StructCodec.struct( "name", Codec.STRING, Fixed::name, Fixed::new ); @Override public @NotNull Function<@NotNull String, @Nullable Integer> apply(@NotNull LootContext context) { throw new UnsupportedOperationException("TODO: Implement entity scores (String name -> String objective -> @Nullable Integer)"); } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/LootTable.java ================================================ package net.minestom.vanilla.loot; import net.kyori.adventure.key.Key; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; /** * A loot table. * @param pools the pools that generate items in this table * @param functions the functions applied to each output item of this table * @param randomSequence An ID specifying the name of the random sequence that is used to generate loot from this loot table. */ public record LootTable(@NotNull List pools, @NotNull List functions, @Nullable Key randomSequence) implements LootGenerator { public static final @NotNull LootTable EMPTY = new LootTable(List.of(), List.of(), null); @SuppressWarnings("UnstableApiUsage") public static final @NotNull StructCodec CODEC = StructCodec.struct( "pools", LootPool.CODEC.list().optional(List.of()), LootTable::pools, "functions", LootFunction.CODEC.list().optional(List.of()), LootTable::functions, "random_sequence", Codec.KEY.optional(), LootTable::randomSequence, LootTable::new ); @Override public @NotNull List generate(@NotNull LootContext context) { List items = new ArrayList<>(); for (var pool : pools) { for (var item : pool.generate(context)) { items.add(LootFunction.apply(functions, item, context)); } } return items; } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/EnchantmentUtils.java ================================================ package net.minestom.vanilla.loot.util; import net.minestom.server.component.DataComponent; import net.minestom.server.component.DataComponents; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.LivingEntity; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.registry.RegistryKey; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; public class EnchantmentUtils { private EnchantmentUtils() { } public static int level(@Nullable ItemStack item, @NotNull RegistryKey key) { if (item == null) return 0; EnchantmentList enchantments = item.get(DataComponents.ENCHANTMENTS); if (enchantments == null) return 0; return enchantments.enchantments().getOrDefault(key, 0); } public static int level(@Nullable Entity entity, @NotNull RegistryKey key) { int level = 0; if (entity instanceof LivingEntity living) { for (EquipmentSlot slot : EquipmentSlot.values()) { EnchantmentList ench = living.getEquipment(slot).get(DataComponents.ENCHANTMENTS); if (ench == null) continue; level = Math.max(level, ench.level(key)); } } return level; } public static @NotNull ItemStack modifyItem(@NotNull ItemStack item, @NotNull Consumer, Integer>> enchantments) { DataComponent type = item.material().equals(Material.ENCHANTED_BOOK) ? DataComponents.STORED_ENCHANTMENTS : DataComponents.ENCHANTMENTS; EnchantmentList component = item.get(type, EnchantmentList.EMPTY); var map = new HashMap<>(component.enchantments()); enchantments.accept(map); // Make the book enchanted! if (!map.isEmpty() && item.material().equals(Material.BOOK)) { return item.builder() .material(Material.ENCHANTED_BOOK) .set(DataComponents.STORED_ENCHANTMENTS, new EnchantmentList(map)) .build(); } else { return item.with(type, new EnchantmentList(map)); } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/ListOperation.java ================================================ package net.minestom.vanilla.loot.util; import net.kyori.adventure.key.Key; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.server.registry.DynamicRegistry; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @SuppressWarnings("UnstableApiUsage") public sealed interface ListOperation { @NotNull StructCodec CODEC = Codec.RegistryTaggedUnion(registries -> { class Holder { static final @NotNull DynamicRegistry> CODEC = createDefaultRegistry(); } return Holder.CODEC; }, ListOperation::codec, "type"); static @NotNull DynamicRegistry> createDefaultRegistry() { final DynamicRegistry> registry = DynamicRegistry.create(Key.key("list_operations")); registry.register("append", Append.CODEC); registry.register("insert", Insert.CODEC); registry.register("replace_all", ReplaceAll.CODEC); registry.register("replace_section", ReplaceSection.CODEC); return registry; } @NotNull List apply(@NotNull List values, @NotNull List input); /** * @return the codec that can encode this list operation */ @NotNull StructCodec codec(); record WithValues(@NotNull ListOperation operation, @NotNull List values) { public static Codec> codec(Codec codec) { return StructCodec.struct( StructCodec.INLINE, ListOperation.CODEC, WithValues::operation, "values", codec.list(), WithValues::values, WithValues::new ); } } record Append() implements ListOperation { public static final @NotNull StructCodec CODEC = StructCodec.struct(Append::new); @Override public @NotNull List apply(@NotNull List values, @NotNull List input) { return Stream.concat(input.stream(), values.stream()).toList(); } @Override public @NotNull StructCodec codec() { return CODEC; } } record Insert(int offset) implements ListOperation { public static final @NotNull StructCodec CODEC = StructCodec.struct( "offset", Codec.INT.optional(0), Insert::offset, Insert::new ); @Override public @NotNull List apply(@NotNull List values, @NotNull List input) { List items = new ArrayList<>(); items.addAll(input.subList(0, this.offset)); items.addAll(values); items.addAll(input.subList(this.offset, input.size())); return items; } @Override public @NotNull StructCodec codec() { return CODEC; } } record ReplaceAll() implements ListOperation { public static final @NotNull StructCodec CODEC = StructCodec.struct(ReplaceAll::new); @Override public @NotNull List apply(@NotNull List values, @NotNull List input) { return values; } @Override public @NotNull StructCodec codec() { return CODEC; } } record ReplaceSection(int offset, @Nullable Integer size) implements ListOperation { public static final @NotNull StructCodec CODEC = StructCodec.struct( "offset", Codec.INT.optional(0), ReplaceSection::offset, "size", Codec.INT.optional(), ReplaceSection::size, ReplaceSection::new ); @Override public @NotNull List apply(@NotNull List values, @NotNull List input) { List items = new ArrayList<>(); items.addAll(input.subList(0, offset)); items.addAll(values); int size = this.size != null ? this.size : values.size(); // Add truncated part of list of possible if (offset + size < input.size()) { items.addAll(input.subList(offset + size, input.size())); } return items; } @Override public @NotNull StructCodec codec() { return CODEC; } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/LootNumberRange.java ================================================ package net.minestom.vanilla.loot.util; import net.minestom.server.codec.Codec; import net.minestom.server.codec.StructCodec; import net.minestom.vanilla.loot.LootContext; import net.minestom.vanilla.loot.LootNumber; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * An inclusive number range based on loot numbers. * @param min the optional minimum value * @param max the optional maximum value */ public record LootNumberRange(@Nullable LootNumber min, @Nullable LootNumber max) { @SuppressWarnings({"UnstableApiUsage", "NumberEquality"}) public static final @NotNull Codec CODEC = Codec.DOUBLE.transform(LootNumber.Constant::new, LootNumber.Constant::value) .transform(n -> new LootNumberRange(n, n), r -> { if (r.min() instanceof LootNumber.Constant(Double min) && r.max() instanceof LootNumber.Constant(Double max) && min == max) { return (LootNumber.Constant) r.min(); } else throw new UnsupportedOperationException("Using struct codec"); }).orElse(StructCodec.struct( "min", LootNumber.CODEC.optional(), LootNumberRange::min, "max", LootNumber.CODEC.optional(), LootNumberRange::max, LootNumberRange::new )); /** * Limits the provided value to between the minimum and maximum.
* This API currently guarantees that, if the minimum ends up being larger than the maximum, the resulting value * will be equal to the maximum. * @param context the context, to use for getting the values of the min and max * @param number the number to constrain to between the minimum and maximum * @return the constrained number */ public long limit(@NotNull LootContext context, long number) { if (this.min != null) { number = Math.max(this.min.getInt(context), number); } if (this.max != null) { number = Math.min(this.max.getInt(context), number); } return number; } /** * Limits the provided value to between the minimum and maximum.
* This API currently guarantees that, if the minimum ends up being larger than the maximum, the resulting value * will be equal to the maximum. * @param context the context, to use for getting the values of the min and max * @param number the number to constrain to between the minimum and maximum * @return the constrained number */ public double limit(@NotNull LootContext context, double number) { if (this.min != null) { number = Math.max(this.min.getDouble(context), number); } if (this.max != null) { number = Math.min(this.max.getDouble(context), number); } return number; } /** * Assures that the provided number is not smaller than the minimum and is not larger than the maximum. If either of * the bounds is null, it's always considered as passing. * @param context the context, to use for getting the values of the min and max * @param number the number to check the validity of * @return true if the provided number fits within {@link #min()} and {@link #max()}, and false otherwise */ public boolean check(@NotNull LootContext context, long number) { return (this.min == null || this.min.getInt(context) <= number) && (this.max == null || this.max.getInt(context) >= number); } /** * Assures that the provided number is not smaller than the minimum and is not larger than the maximum. If either of * the bounds is null, it's always considered as passing. * @param context the context, to use for getting the values of the min and max * @param number the number to check the validity of * @return true if the provided number fits within {@link #min()} and {@link #max()}, and false otherwise */ public boolean check(@NotNull LootContext context, double number) { return (this.min == null || this.min.getDouble(context) <= number) && (this.max == null || this.max.getDouble(context) >= number); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/RelevantEntity.java ================================================ package net.minestom.vanilla.loot.util; import net.minestom.server.codec.Codec; import net.minestom.server.entity.Entity; import net.minestom.vanilla.loot.LootContext; import org.jetbrains.annotations.NotNull; public enum RelevantEntity { THIS("this", LootContext.THIS_ENTITY), ATTACKER("attacker", LootContext.ATTACKING_ENTITY), DIRECT_ATTACKER("direct_attacker", LootContext.DIRECT_ATTACKING_ENTITY), LAST_PLAYER_DAMAGE("killer_player", LootContext.LAST_DAMAGE_PLAYER); @SuppressWarnings("UnstableApiUsage") public static final @NotNull Codec CODEC = Codec.Enum(RelevantEntity.class); // Relies on the enum names themselves being accurate private final @NotNull String id; private final @NotNull LootContext.Key key; RelevantEntity(@NotNull String id, @NotNull LootContext.Key key) { this.id = id; this.key = key; } public @NotNull String id() { return id; } public @NotNull LootContext.Key key() { return key; } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/RelevantTarget.java ================================================ package net.minestom.vanilla.loot.util; import net.minestom.server.codec.Codec; import net.minestom.vanilla.loot.LootContext; import org.jetbrains.annotations.NotNull; public enum RelevantTarget { THIS("this", LootContext.THIS_ENTITY), ATTACKING_ENTITY("attacking_entity", LootContext.ATTACKING_ENTITY), LAST_DAMAGE_PLAYER("last_damage_player", LootContext.LAST_DAMAGE_PLAYER), BLOCK_ENTITY("block_entity", LootContext.BLOCK_STATE); @SuppressWarnings("UnstableApiUsage") public static final Codec CODEC = Codec.Enum(RelevantTarget.class); // Relies on the enum names themselves being accurate private final @NotNull String id; private final @NotNull LootContext.Key key; RelevantTarget(@NotNull String id, @NotNull LootContext.Key key) { this.id = id; this.key = key; } public @NotNull String id() { return id; } public @NotNull LootContext.Key key() { return key; } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/nbt/NBTPath.java ================================================ package net.minestom.vanilla.loot.util.nbt; import it.unimi.dsi.fastutil.ints.IntSet; import net.kyori.adventure.nbt.*; import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.codec.Codec; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; /** * A NBTPath allows selecting specific elements from a NBT tree, based on a list of selectors. Each selector has the * ability to select any number of elements from its predecessor—this allows arbitrary item selection from any NBT type. *
* It also provides multiple ways to manipulate NBT results as there is no deeply mutable NBT implementation. */ public record NBTPath(@NotNull List selectors) { @SuppressWarnings("UnstableApiUsage") public static final @NotNull Codec CODEC = Parser.CODEC; /** * Selects an arbitrary number of elements from provided NBT. */ public sealed interface Selector { /** * Passes each selected NBT element into the given consumer. */ void get(@NotNull NBTReference source, @NotNull Consumer consumer); /** * Modifies the provided {@code source} so that, if possible, this path selector will select at least one NBT * element from it. If a placeholder element is needed, {@code nextElement} is to be used. */ void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement); /** * Provides a tag that this selector may be willing to modify. */ @NotNull BinaryTag preparedNBT(); record RootKey(@NotNull String key) implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { if (source.has(key)) { consumer.accept(source.get(key)); } } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) {} @Override public @NotNull BinaryTag preparedNBT() { return CompoundBinaryTag.empty(); } @Override public String toString() { return key; } } record Key(@NotNull String key) implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { if (source.has(key)) { consumer.accept(source.get(key)); } } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) { if (!source.has(key)) { source.get(key).set(nextElement.get()); } } @Override public @NotNull BinaryTag preparedNBT() { return CompoundBinaryTag.empty(); } @Override public String toString() { return "." + key; } } record CompoundFilter(@NotNull CompoundBinaryTag filter) implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { if (NBTUtils.compareNBT(filter, source.get(), false)) { consumer.accept(source); } } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) { if (!NBTUtils.compareNBT(filter, source.get(), false)) { source.set(filter); } } @Override public @NotNull BinaryTag preparedNBT() { return CompoundBinaryTag.empty(); } @Override public String toString() { try { return TagStringIO.get().asString(filter); } catch (IOException e) { throw new RuntimeException(e); } } } record EntireList() implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { int size = source.listSize(); for (int i = 0; i < size; i++) { consumer.accept(source.get(i)); } } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) { if (source.listSize() == 0) { source.listAdd(nextElement.get()); } } @Override public @NotNull BinaryTag preparedNBT() { return ListBinaryTag.empty(); } @Override public String toString() { return "[]"; } } record Index(int index) implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { var newIndex = index >= 0 ? index : source.listSize() + index; if (newIndex < 0 || newIndex >= source.listSize()) return; consumer.accept(source.get(newIndex)); } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) {} @Override public @NotNull BinaryTag preparedNBT() { return ListBinaryTag.empty(); } @Override public String toString() { return "[" + index + "]"; } } record ListFilter(@NotNull CompoundBinaryTag filter) implements Selector { @Override public void get(@NotNull NBTReference source, @NotNull Consumer consumer) { int listSize = source.listSize(); for (int i = 0; i < listSize; i++) { NBTReference ref = source.get(i); if (NBTUtils.compareNBT(filter, ref.get(), false)) { consumer.accept(ref); } } } @Override public void prepare(@NotNull NBTReference source, @NotNull Supplier nextElement) { int listSize = source.listSize(); if (listSize == -1) return; for (int i = 0; i < listSize; i++) { if (NBTUtils.compareNBT(filter, source.get(i).get(), false)) { return; } } source.listAdd(filter); } @Override public @NotNull BinaryTag preparedNBT() { return ListBinaryTag.empty(); } @Override public String toString() { try { return "[" + TagStringIO.get().asString(filter) + "]"; } catch (IOException e) { throw new RuntimeException(e); } } } } /** * Strings {@code source} through each selector in {@link #selectors()}, returning the selected results. It is * possible for there to be none. Modifying the resulting NBT references does nothing. * @param source the source, to get the NBT from * @return the list of selected NBT, which may be empty */ public @NotNull List get(@NotNull BinaryTag source) { List references = List.of(NBTReference.of(source)); for (var selector : selectors()) { List newReferences = new ArrayList<>(); references.forEach(nbt -> selector.get(nbt, newReferences::add)); if (newReferences.isEmpty()) { return List.of(); } references = newReferences; } return references; } /** * Strings {@code source} through each selector, making each selector * {@link Selector#prepare(NBTReference, Supplier) prepare} each element. This should result in this method * returning nothing much less often, although it is still possible. Modifying the resulting NBT references will * result in the provided {@code source} being modified. * @param source the source, to get the NBT from * @param finalDefault the default value for results produced from the last path selector * @return the list of selected NBT, which may be empty */ public @NotNull List getWithDefaults(@NotNull NBTReference source, @NotNull Supplier finalDefault) { List references = List.of(source); for (int selectorIndex = 0; selectorIndex < selectors().size(); selectorIndex++) { var selector = selectors().get(selectorIndex); Supplier next = (selectorIndex < selectors().size() - 1) ? selectors().get(selectorIndex + 1)::preparedNBT : finalDefault; List newNBT = new ArrayList<>(); for (var nbt : references) { selector.prepare(nbt, next); selector.get(nbt, newNBT::add); } if (newNBT.isEmpty()) { return List.of(); } references = newNBT; } return references; } /** * Strings {@code source} through each selector, making each selector * {@link Selector#prepare(NBTReference, Supplier) prepare} each element. This should result in this method * returning nothing much less often, although it is still possible. Modifying the resulting NBT references will * result in the provided {@code source} being modified. This is equivalent to calling * {@link #getWithDefaults(NBTReference, Supplier)} and setting all of the results to {@code setValue}. * @param source the source, to get the NBT from * @param setValue the value to set all selected elements to * @return the list of selected NBT, which may be empty */ public @NotNull List set(@NotNull NBTReference source, @NotNull BinaryTag setValue) { List references = getWithDefaults(source, () -> setValue); for (var reference : references) { reference.set(setValue); } return references; } @Override public String toString() { return selectors().stream().map(Selector::toString).collect(Collectors.joining()); } } @SuppressWarnings("UnstableApiUsage") class Parser { static final @NotNull IntSet VALID_SELECTOR_STARTERS = IntSet.of('.', '{', '['); static final @NotNull IntSet VALID_INTEGER_CHARACTERS = IntSet.of('-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); static final @NotNull IntSet INVALID_UNQUOTED_CHARACTERS = IntSet.of(-1, '.', '\'', '\"', '{', '}', '[', ']'); static final Codec CODEC = Codec.STRING.transform(s -> { try { return readPath(new StringReader(s)); } catch (IOException e) { throw new RuntimeException(e); } }, NBTPath::toString); static @NotNull NBTPath readPath(@NotNull StringReader reader) throws IOException { List selectors = new ArrayList<>(); if (!VALID_SELECTOR_STARTERS.contains(peek(reader))) { var key = readString(reader); if (key != null) { selectors.add(new NBTPath.Selector.RootKey(key)); } } while (true) { reader.mark(0); if (!VALID_SELECTOR_STARTERS.contains(peek(reader))) { if (selectors.isEmpty()) { throw new IllegalArgumentException("NBT paths must contain at least one selector"); } return new NBTPath(List.copyOf(selectors)); } var selector = readPathSelector(reader); if (selector == null) { throw new IllegalArgumentException("Invalid NBT path selector"); } selectors.add(selector); } } // Returning null indicates a failure to read @SuppressWarnings("ResultOfMethodCallIgnored") static @Nullable NBTPath.Selector readPathSelector(@NotNull StringReader reader) throws IOException { var firstChar = peek(reader); return switch (firstChar) { case '.' -> { reader.skip(1); // Skip period var string = readString(reader); yield string != null ? new NBTPath.Selector.Key(string) : null; } case '{' -> { var compound = readCompound(reader); yield compound != null ? new NBTPath.Selector.CompoundFilter(compound) : null; } case '[' -> { reader.skip(1); // Skip opening square brackets var secondChar = peek(reader); var selector = switch(secondChar) { case ']' -> new NBTPath.Selector.EntireList(); case '{' -> { var compound = readCompound(reader); yield compound != null ? new NBTPath.Selector.ListFilter(compound) : null; } default -> { if (VALID_INTEGER_CHARACTERS.contains(secondChar)) { var index = readInteger(reader); yield index != null ? new NBTPath.Selector.Index(index) : null; } yield null; } }; reader.skip(1); // Skip closing square brackets yield selector; } default -> null; }; } @SuppressWarnings("ResultOfMethodCallIgnored") private static @Nullable Integer readInteger(@NotNull StringReader reader) throws IOException { StringBuilder builder = new StringBuilder(); int peek; while (VALID_INTEGER_CHARACTERS.contains(peek = reader.read())) { builder.appendCodePoint(peek); } // Unread the one extra character that was read; this does nothing if the entire string has been read reader.skip(-1); try { return Integer.parseInt(builder.toString()); } catch (NumberFormatException e) { return null; } } @SuppressWarnings("ResultOfMethodCallIgnored") private static @Nullable String readString(@NotNull StringReader reader) throws IOException { var peek = peek(reader); if (peek == -1) { return null; } StringBuilder builder = new StringBuilder(); if (peek == '"' || peek == '\'') { // Read quoted string reader.skip(1); // Skip the character we know already boolean escape = false; while (true) { var next = reader.read(); if (next == '\\') { // Read escape character escape = true; } else if (next == peek && !escape) { // Return if unescaped closing character return builder.toString(); } else { if (escape) { // If there was an unused escape, re-add it; there's only one character it's used for builder.appendCodePoint('\\'); } if (next == -1) { return null; } builder.appendCodePoint(next); // Add the next character always } } } // Read unquoted string int read; while (!INVALID_UNQUOTED_CHARACTERS.contains(read = reader.read())) { builder.appendCodePoint(read); } // Unread the one extra character that was read; this does nothing if the entire string has been read reader.skip(-1); return builder.isEmpty() ? null : builder.toString(); } @SuppressWarnings("ResultOfMethodCallIgnored") private static int peek(@NotNull StringReader reader) throws IOException { var codePoint = reader.read(); reader.skip(-1); return codePoint; } @SuppressWarnings("ResultOfMethodCallIgnored") private static BinaryTag readTag(@NotNull StringReader reader) throws IOException { StringBuilder builder = new StringBuilder(); while (true) { int code = reader.read(); if (code == -1) break; builder.appendCodePoint(code); } String dump = builder.toString(); StringBuffer buffer = new StringBuffer(); BinaryTag tag = MinestomAdventure.tagStringIO().asTag(dump, buffer); reader.skip(dump.length() - buffer.length()); // Skip (total chars) - (remaining chars) = read chars return tag; } private static CompoundBinaryTag readCompound(@NotNull StringReader reader) throws IOException { if (readTag(reader) instanceof CompoundBinaryTag compound) { return compound; } else { throw new IllegalArgumentException("Expected an inline compound"); } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/nbt/NBTReference.java ================================================ package net.minestom.vanilla.loot.util.nbt; import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.ListBinaryTag; import org.jetbrains.annotations.NotNull; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; public record NBTReference(@NotNull Supplier getter, @NotNull Consumer setter) { public static @NotNull NBTReference of(@NotNull BinaryTag tag) { AtomicReference reference = new AtomicReference<>(tag); return new NBTReference(reference::get, reference::set); } public BinaryTag get() { return getter.get(); } public void set(BinaryTag nbt) { setter.accept(nbt); } public boolean has(@NotNull String key) { return get() instanceof CompoundBinaryTag compound && compound.get(key) != null; } public NBTReference get(@NotNull String key) { return new NBTReference( () -> get() instanceof CompoundBinaryTag compound ? compound.get(key) : null, nbt -> { if (get() instanceof CompoundBinaryTag compound) { set(compound.put(key, nbt)); } } ); } public int listSize() { return get() instanceof ListBinaryTag list ? list.size() : -1; } public @NotNull NBTReference get(int index) { return new NBTReference( () -> get() instanceof ListBinaryTag list && index >= 0 && index < list.size() ? list.get(index) : null, value -> { if (get() instanceof ListBinaryTag list && (value.type().equals(BinaryTagTypes.END) || list.elementType().equals(value.type())) && index >= 0 && index < list.size()) { set(list.set(index, value, null)); } } ); } public void listAdd(@NotNull BinaryTag tag) { if (get() instanceof ListBinaryTag list && (list.isEmpty() || list.elementType().equals(tag.type()))) { set(list.add(tag)); } } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/nbt/NBTUtils.java ================================================ package net.minestom.vanilla.loot.util.nbt; import net.kyori.adventure.nbt.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.CharBuffer; import java.util.Map; public class NBTUtils { private NBTUtils() {} /** * Checks to see if everything in {@code standard} is contained in {@code comparison}. The comparison is allowed to * have extra fields that are not contained in the standard. * @param standard the standard that the comparison must have all elements of * @param comparison the comparison, that is being compared against the standard. NBT compounds in this parameter, * whether deeper in the tree or not, are allowed to have keys that the standard does not - it's * basically compared against a standard. * @param assureListOrder whether or not to assure list order. When true, lists are directly compared, but when * false, the comparison is checked to see if it contains each item in the standard. * @return true if the comparison fits the standard, otherwise false */ public static boolean compareNBT(@Nullable BinaryTag standard, @Nullable BinaryTag comparison, boolean assureListOrder) { if (standard == null) { return true; // If there's no standard, it must always pass } else if (comparison == null) { return false; // If it's null at this point, we already assured that standard is null, so it must be invalid } else if (!standard.type().equals(comparison.type())) { return false; // If the classes aren't equal it can't fulfill the standard anyway } // If the list order is assured, it will be handled with the simple #equals call later in the method if (!assureListOrder && standard instanceof ListBinaryTag guaranteeList) { ListBinaryTag comparisonList = ((ListBinaryTag) comparison); if (guaranteeList.isEmpty()) { return comparisonList.isEmpty(); } for (BinaryTag nbt : guaranteeList) { boolean contains = false; for (BinaryTag compare : comparisonList) { if (compareNBT(nbt, compare, false)) { contains = true; break; } } if (!contains) { return false; } } return true; } if (standard instanceof CompoundBinaryTag standardCompound) { CompoundBinaryTag comparisonCompound = ((CompoundBinaryTag) comparison); for (String key : standardCompound.keySet()) { if (!compareNBT(comparisonCompound.get(key), comparisonCompound.get(key), assureListOrder)) { return false; } } return true; } return standard.equals(comparison); } /** * Merges the two provided compounds, preferring the value of the {@code changes} compound and merging any nested * NBT compounds like it would for the first-level ones. * @param base the base compound, to be merged onto * @param changes the changes to make to the base compound * @return the merged compound */ public static CompoundBinaryTag merge(@NotNull CompoundBinaryTag base, @NotNull CompoundBinaryTag changes) { CompoundBinaryTag.Builder result = CompoundBinaryTag.builder(); result.put(base); changes.iterator().forEachRemaining((entry) -> { BinaryTag value = entry.getValue(); if (base.get(entry.getKey()) instanceof CompoundBinaryTag baseCompound && entry.getValue() instanceof CompoundBinaryTag changeCompound) { value = NBTUtils.merge(baseCompound, changeCompound); } result.put(entry.getKey(), value); }); return result.build(); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/predicate/DamageSourcePredicate.java ================================================ package net.minestom.vanilla.loot.util.predicate; import net.minestom.server.codec.Codec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.instance.Instance; import net.minestom.server.utils.Unit; import org.jetbrains.annotations.NotNull; // TODO: Incomplete @SuppressWarnings("UnstableApiUsage") public interface DamageSourcePredicate { @NotNull Codec CODEC = Codec.UNIT.transform(a -> (instance, pos, type) -> false, a -> Unit.INSTANCE); boolean test(@NotNull Instance instance, @NotNull Point pos, @NotNull DamageType type); } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/predicate/EntityPredicate.java ================================================ package net.minestom.vanilla.loot.util.predicate; import net.minestom.server.codec.Codec; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; import net.minestom.server.utils.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; // TODO: Incomplete @SuppressWarnings("UnstableApiUsage") public interface EntityPredicate { @NotNull Codec CODEC = Codec.UNIT.transform(a -> (instance, pos, entity) -> false, a -> Unit.INSTANCE); boolean test(@NotNull Instance instance, @Nullable Point pos, @Nullable Entity entity); } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/predicate/ItemPredicate.java ================================================ package net.minestom.vanilla.loot.util.predicate; import net.minestom.server.codec.StructCodec; import net.minestom.server.instance.block.predicate.DataComponentPredicates; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.registry.Registries; import net.minestom.server.registry.RegistryTag; import net.minestom.vanilla.loot.LootContext; import net.minestom.vanilla.loot.util.LootNumberRange; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @SuppressWarnings("UnstableApiUsage") public record ItemPredicate(@Nullable RegistryTag items, @NotNull LootNumberRange count, @NotNull DataComponentPredicates predicate) { public static final @NotNull StructCodec CODEC = StructCodec.struct( "items", RegistryTag.codec(Registries::material).optional(), ItemPredicate::items, "count", LootNumberRange.CODEC.optional(new LootNumberRange(null, null)), ItemPredicate::count, StructCodec.INLINE, DataComponentPredicates.CODEC, ItemPredicate::predicate, ItemPredicate::new ); public boolean test(@NotNull ItemStack itemStack, @NotNull LootContext context) { if (items != null && !items.contains(itemStack.material())) return false; return count.check(context, itemStack.amount()) && false; // TODO: Waiting for #2732 // return count.check(context, itemStack.amount()) && predicate.test(itemStack); } } ================================================ FILE: loot-table/src/main/java/net/minestom/vanilla/loot/util/predicate/LocationPredicate.java ================================================ package net.minestom.vanilla.loot.util.predicate; import net.minestom.server.codec.Codec; import net.minestom.server.coordinate.Point; import net.minestom.server.instance.Instance; import net.minestom.server.utils.Unit; import org.jetbrains.annotations.NotNull; // TODO: Incomplete @SuppressWarnings("UnstableApiUsage") public interface LocationPredicate { @NotNull Codec CODEC = Codec.UNIT.transform(a -> (instance, point) -> false, a -> Unit.INSTANCE); boolean test(@NotNull Instance instance, @NotNull Point point); } ================================================ FILE: mojang-data/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) } ================================================ FILE: mojang-data/src/main/java/io/github/pesto/MojangAssets.java ================================================ package io.github.pesto; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import net.minestom.vanilla.logging.Loading; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.CompletableFuture; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; final class MojangAssets { private static final File ROOT = new File(".", "mojang-data"); private static final String VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; public CompletableFuture> getAssets(@NotNull String version) { return CompletableFuture.supplyAsync(() -> downloadResources(version)); } private FileSystem downloadResources(@NotNull String version) { try { Loading.start("Downloading vanilla jar (" + version + ")..."); // Check if source files already exist File jar = new File(ROOT, version + File.separator + "resources.jar"); if (!jar.exists()) { // Get version info String versionInfoUrl = findVersionInfoUrl(version); JsonObject versionInfo = downloadJson(versionInfoUrl); // Download jar downloadJar(versionInfo, jar); } return FileSystem.fromZipFile(jar, path -> path.startsWith("data/minecraft/")).folder("data"); } catch (IOException e) { exitError(e.getMessage()); } finally { Loading.finish(); } return FileSystem.empty(); } /** * Gets the version info from the version manifest * * @param version The release version or latest version if requested * @return The url to the version info * @throws IOException If the version info url could not be found */ private String findVersionInfoUrl(@NotNull String version) throws IOException { // Get manifest JsonObject manifest = downloadJson(VERSION_MANIFEST_URL); // Get the latest version if requested if (version.equals("latest")) { JsonObject latest = manifest.getAsJsonObject("latest"); version = latest.get("release").getAsString(); } // Find the url for the version's info for (JsonElement elem : manifest.getAsJsonArray("versions")) { JsonObject info = elem.getAsJsonObject(); String id = info.get("id").getAsString(); if (!id.equals(version)) continue; return info.get("url").getAsString(); } throw new IOException("Failed to find version info url for version " + version); } /** * Downloads the vanilla jar to be used for extracting * * @param versionInfo The version info */ private void downloadJar(JsonObject versionInfo, @NotNull File destination) throws IOException { JsonObject downloads = versionInfo.getAsJsonObject("downloads"); JsonObject client = downloads.getAsJsonObject("client"); String url = client.get("url").getAsString(); // Create if it doesn't exist if (!destination.exists()) { destination.getParentFile().mkdirs(); destination.createNewFile(); } URLConnection connection = new URL(url).openConnection(); connection.connect(); try (InputStream input = connection.getInputStream()) { // Download the jar to memory first ByteBuffer buffer = ByteBuffer.allocateDirect(connection.getContentLength()); double totalMB = (double) connection.getContentLengthLong() / 1024 / 1024; Loading.start(String.format("Downloading vanilla jar (%.2f MB)...", totalMB)); long pos = 0; long segmentCompleted = 0; while (true) { var bytes = input.readNBytes(64); if (bytes.length == 0) break; pos += bytes.length; buffer.put(bytes); // we only want to update the progress every 8th of the total size double progress = (double) pos / (double) connection.getContentLengthLong(); if (progress - segmentCompleted > 1.0 / 8.0) { segmentCompleted = (long) (progress * 8.0) / 8; Loading.updater().progress(progress); } } Loading.finish(); // Write the buffer to the file buffer.flip(); try (FileChannel channel = FileChannel.open(destination.toPath(), WRITE, TRUNCATE_EXISTING)) { channel.write(buffer); } boolean success = destination.exists() && destination.length() == connection.getContentLengthLong(); if (!success) throw new IOException("Failed to download client JAR"); } } // private boolean extractJarAssets(@NotNull File jarFile, File root) { // File output = new File(root, jarFile.getParentFile().getName()); // // try (ZipInputStream zip = new ZipInputStream(new FileInputStream(jarFile))) { // ZipEntry entry; // while ((entry = zip.getNextEntry()) != null) { // // // Skip unrelated entries // if (!entry.getName().startsWith("data")) // continue; // // // Validate that the file exists // File file = checkAndCreateFile(output, entry); // if (file.exists()) continue; // // if (entry.isDirectory()) { // if (!file.isDirectory() && !file.mkdirs()) // throw new IOException("Failed to create directory " + file); // } else { // File parent = file.getParentFile(); // if (!parent.isDirectory() && !parent.mkdirs()) // throw new IOException("Failed to create directory " + parent); // // Files.copy(zip, file.toPath()); // } // } // zip.closeEntry(); // jarFile.delete(); // // return true; // } catch (IOException e) { // e.printStackTrace(); // } // return false; // } private JsonObject downloadJson(String url) throws IOException { return JsonParser.parseReader(new InputStreamReader(getDownloadStream(url))).getAsJsonObject(); } private InputStream getDownloadStream(String url) throws IOException { return new URL(url).openStream(); } private void exitError(String message) { System.err.println(message); System.exit(1); } } ================================================ FILE: mojang-data/src/main/java/io/github/pesto/MojangDataFeature.java ================================================ package io.github.pesto; import net.kyori.adventure.key.Key; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.files.ByteArray; import net.minestom.vanilla.files.FileSystem; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; public class MojangDataFeature implements VanillaReimplementation.Feature { private static final String LATEST = "1.21.5"; private final MojangAssets assets = new MojangAssets(); private final CompletableFuture> latest = new CompletableFuture<>(); @Override public void hook(@NotNull HookContext context) { assetsRequest(LATEST) .thenAccept(latest::complete) .join(); } @Override public @NotNull Key key() { return Key.key("io_github_pesto:mojang_data"); } public FileSystem latestAssets() { if (!latest.isDone()) { throw new IllegalStateException("Cannot request assets before {@link MojangDataFeature} is loaded"); } return latest.join(); } public CompletableFuture> assetsRequest(@NotNull String version) { return assets.getAssets(version); } } ================================================ FILE: server/build.gradle.kts ================================================ // Find all projects except for the root project and this project. val disallowed = setOf(project.name, project.parent!!.name) val includedProjects = (project.parent?.allprojects ?: emptyList()).filter { !disallowed.contains(it.name) } dependencies { includedProjects.forEach { api(project(":" + it.name)) } } tasks { withType(com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class.java) { minimize { includedProjects.forEach { exclude(project(":" + it.name)) } } manifest { attributes( "Main-Class" to "net.minestom.vanilla.server.VanillaServer", "Multi-Release" to true ) } mergeServiceFiles() } } ================================================ FILE: server/src/main/java/net/minestom/vanilla/server/VanillaDebug.java ================================================ package net.minestom.vanilla.server; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.VanillaRegistry; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.entitymeta.EntityTags; import java.util.Random; /** * This debug server can be edited and committed to master without any consequences. * Make sure to keep anything cool you make! A quick comment is always a good idea. *

* And try not to remove anyone else's additions, just comment them out. */ public class VanillaDebug { public static void hook(VanillaServer server) { VanillaReimplementation vri = server.vri(); vri.process().eventHandler() .addListener(PlayerChatEvent.class, event -> handleMessage(server, event.getPlayer(), event.getRawMessage())) // .addListener(PlayerTickEvent.class, event -> { // if (event.getPlayer().getInstance().getWorldAge() % 10 == 0) { // handleMessage(server, event.getPlayer(), "fallingblock"); // } // }) ; } private static void handleMessage(VanillaServer server, Player player, String message) { VanillaReimplementation vri = server.vri(); Block[] blocks = Block.values().toArray(new Block[0]); Random random = new Random(); switch (message) { case "tnt" -> { Pos pos = player.getPosition(); VanillaRegistry.EntityContext context = vri.entityContext(EntityType.TNT, pos); Entity entity = vri.createEntityOrDummy(context); entity.setInstance(server.overworld(), pos); } case "fallingblock" -> { Pos pos = player.getPosition(); VanillaRegistry.EntityContext context = vri.entityContext(EntityType.FALLING_BLOCK, pos, tags -> tags.setTag(EntityTags.FallingBlock.BLOCK, blocks[random.nextInt(blocks.length)])); Entity entity = vri.createEntityOrDummy(context); entity.setInstance(server.overworld(), pos); } } } } ================================================ FILE: server/src/main/java/net/minestom/vanilla/server/VanillaEvents.java ================================================ package net.minestom.vanilla.server; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.event.Event; import net.minestom.server.event.EventListener; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.AddEntityToInstanceEvent; import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.event.player.*; import net.minestom.server.extras.MojangAuth; import net.minestom.server.instance.ExplosionSupplier; import net.minestom.server.instance.Instance; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.packet.client.ClientPacket; import net.minestom.server.network.packet.client.play.ClientPlayerPositionAndRotationPacket; import net.minestom.server.network.packet.client.play.ClientPlayerPositionPacket; import net.minestom.server.network.packet.client.play.ClientPlayerRotationPacket; import net.minestom.server.utils.time.TimeUnit; import net.minestom.vanilla.generation.VanillaTestGenerator; import net.minestom.vanilla.instance.VanillaExplosion; import net.minestom.vanilla.logging.Logger; import net.minestom.vanilla.system.ServerProperties; public class VanillaEvents { public static void register(VanillaServer server, ServerProperties properties, EventNode eventNode) { String worldName = properties.get("level-name"); ExplosionSupplier explosionGenerator = (centerX, centerY, centerZ, strength, additionalData) -> { boolean isTNT = additionalData != null && Boolean.TRUE.equals(additionalData.getBoolean(VanillaExplosion.DROP_EVERYTHING_KEY)); boolean noBlockDamage = additionalData != null && Boolean.TRUE.equals(additionalData.getBoolean(VanillaExplosion.DONT_DESTROY_BLOCKS_KEY)); return VanillaExplosion.builder(new Pos(centerX, centerY, centerZ), strength) .destroyBlocks(!noBlockDamage) .isFlaming(false) .dropEverything(isTNT) .build(); }; // TODO: World storage VanillaTestGenerator noiseTestGenerator = new VanillaTestGenerator(); Instance overworld = server.overworld(); // overworld.enableAutoChunkLoad(true); overworld.setGenerator(noiseTestGenerator); // overworld.setExplosionSupplier(explosionGenerator); // overworld.setChunkLoader(new AnvilChunkLoader(storageManager.getLocation(worldName + "/region"))); // // nether = MinecraftServer.getInstanceManager().createInstanceContainer(VanillaDimensionTypes.NETHER); // nether.enableAutoChunkLoad(true); // nether.setGenerator(noiseTestGenerator); // nether.setExplosionSupplier(explosionGenerator); // nether.setChunkLoader(new AnvilChunkLoader(storageManager.getLocation(worldName + "/DIM-1/region"))); // // end = MinecraftServer.getInstanceManager().createInstanceContainer(VanillaDimensionTypes.END); // end.enableAutoChunkLoad(true); // end.setGenerator(noiseTestGenerator); // end.setExplosionSupplier(explosionGenerator); // end.setChunkLoader(new AnvilChunkLoader(storageManager.getLocation(worldName + "/DIM1/region"))); // Load some chunks beforehand int loopStart = -2; int loopEnd = 2; for (int x = loopStart; x < loopEnd; x++) for (int z = loopStart; z < loopEnd; z++) { overworld.loadChunk(x, z); } eventNode.addListener(AddEntityToInstanceEvent.class, event -> { Entity entity = event.getEntity(); if (entity instanceof Player) { // entity.setTag(NetherPortalBlockHandler.PORTAL_COOLDOWN_TIME_KEY, 5 * 20L); } }); MinecraftServer.getSchedulerManager().buildShutdownTask(() -> { try { overworld.saveInstance(); } catch (Throwable e) { e.printStackTrace(); } }); if (Boolean.parseBoolean(properties.get("online-mode"))) { MojangAuth.init(); } ConnectionManager connectionManager = MinecraftServer.getConnectionManager(); eventNode.addListener( EventListener.of(AsyncPlayerConfigurationEvent.class, event -> { event.setSpawningInstance(overworld); Logger.info(event.getPlayer().getUsername() + " joined the server"); }) ); eventNode.addListener( EventListener.of(PlayerDisconnectEvent.class, event -> Logger.info(event.getPlayer().getUsername() + " left the server")) ); eventNode.addListener( PlayerPacketEvent.class, event -> { ClientPacket packet = event.getPacket(); // moving around and keepalive packets aren't usually required if (packet instanceof ClientPlayerPositionPacket) return; if (packet instanceof ClientPlayerPositionAndRotationPacket) return; if (packet instanceof ClientPlayerRotationPacket) return; Logger.debug("Packet received " + event.getPacket()); } ); eventNode.addListener( PlayerPacketOutEvent.class, playerPacketOutEvent -> { Logger.debug("Packet sent " + playerPacketOutEvent.getPacket()); } ); eventNode.addListener(PlayerMoveEvent.class, event -> { Player player = event.getPlayer(); Point pos = player.getPosition(); Vec vel = player.getVelocity(); double currentX = pos.x(); double currentY = pos.y(); double currentZ = pos.z(); double velocityX = vel.x(); double velocityY = vel.y(); double velocityZ = vel.z(); Point newPos = event.getNewPosition(); double dx = newPos.x() - currentX; double dy = newPos.y() - currentY; double dz = newPos.z() - currentZ; double actualDisplacement = dx * dx + dy * dy + dz * dz; double expectedDisplacement = velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ; float upperLimit = 100; // TODO: 300 if elytra deployed if (actualDisplacement - expectedDisplacement >= upperLimit) { event.setCancelled(true); player.teleport(player.getPosition()); // force teleport to previous position Logger.warn(player.getUsername() + " moved too fast! " + dx + " " + dy + " " + dz); } }); eventNode.addListener( EventListener.builder(PlayerSpawnEvent.class) .filter(PlayerSpawnEvent::isFirstSpawn) .handler(event -> { Player player = event.getPlayer(); player.setPermissionLevel(4); PlayerInventory inventory = player.getInventory(); inventory.addItemStack(ItemStack.of(Material.OBSIDIAN, 1)); inventory.addItemStack(ItemStack.of(Material.FLINT_AND_STEEL, 1)); inventory.addItemStack(ItemStack.of(Material.RED_BED, 1)); inventory.addItemStack(ItemStack.of(Material.CHEST, 1)); inventory.addItemStack(ItemStack.of(Material.CRAFTING_TABLE, 1)); inventory.addItemStack(ItemStack.of(Material.OAK_LOG, 32)); inventory.addItemStack(ItemStack.of(Material.STONECUTTER, 1)); inventory.addItemStack(ItemStack.of(Material.STONE, 64)); }) .build() ); eventNode.addListener( EventListener.builder(PickupItemEvent.class) .filter(event -> event.getEntity() instanceof Player) .handler(event -> { Player player = (Player) event.getEntity(); boolean couldAdd = player.getInventory().addItemStack(event.getItemStack()); event.setCancelled(!couldAdd); // Cancel event if player does not have enough inventory space }) .build() ); eventNode.addListener(ItemDropEvent.class, event -> { Player player = event.getPlayer(); ItemStack droppedItem = event.getItemStack(); ItemEntity itemEntity = new ItemEntity(droppedItem); itemEntity.setPickupDelay(500, TimeUnit.MILLISECOND); itemEntity.setInstance(player.getInstance()); itemEntity.teleport(player.getPosition().add(0, 1.5f, 0)); Vec velocity = player.getPosition().direction().mul(6); itemEntity.setVelocity(velocity); }); } } ================================================ FILE: server/src/main/java/net/minestom/vanilla/server/VanillaServer.java ================================================ package net.minestom.vanilla.server; import net.kyori.adventure.key.Key; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.GameMode; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.extras.lan.OpenToLAN; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.network.ConnectionManager; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.dimensions.VanillaDimensionTypes; import net.minestom.vanilla.logging.Level; import net.minestom.vanilla.logging.Loading; import net.minestom.vanilla.logging.Logger; import net.minestom.vanilla.system.RayFastManager; import net.minestom.vanilla.system.ServerProperties; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; class VanillaServer { /** * A standard vanilla server launch used for testing purposes * * @param args arguments passed from console */ public static void main(String[] args) { // Use the static server process MinecraftServer server = MinecraftServer.init(); // Use SETUP logging level since this is a standalone server. Loading.level(Level.SETUP); Logger.info("Setting up vri..."); VanillaReimplementation vri = VanillaReimplementation.hook(MinecraftServer.process()); VanillaServer vanillaServer = new VanillaServer(server, vri, args); Logger.info("Vanilla Reimplementation (%s) is setup.", MinecraftServer.VERSION_NAME); vanillaServer.start("0.0.0.0", 25565); } private final MinecraftServer minecraftServer; private final @NotNull ServerProperties serverProperties; private final @NotNull VanillaReimplementation vri; // Instances private final @NotNull Instance overworld; public VanillaServer(@NotNull MinecraftServer minecraftServer, @NotNull VanillaReimplementation vri, @Nullable String... args) { this.minecraftServer = minecraftServer; this.serverProperties = getOrGenerateServerProperties(); this.vri = vri; this.overworld = vri.createInstance(Key.key("world"), VanillaDimensionTypes.OVERWORLD); // Try to get server properties // Set up raycasting lib RayFastManager.init(); EventNode eventHandler = MinecraftServer.getGlobalEventHandler(); ConnectionManager connectionManager = MinecraftServer.getConnectionManager(); DimensionType overworldDimension = VanillaDimensionTypes.OVERWORLD; vri.process().scheduler().scheduleNextTick(OpenToLAN::open); // Register systems { // Events VanillaEvents.register(this, serverProperties, eventHandler); } overworld.loadChunk(0, 0).join(); int y = overworldDimension.maxY(); while (Block.AIR.compare(overworld.getBlock(0, y, 0))) { y--; if (y == overworldDimension.minY()) { break; } } int finalY = y; vri.process().eventHandler() .addListener(AsyncPlayerConfigurationEvent.class, event -> { event.setSpawningInstance(overworld); event.getPlayer().setGameMode(GameMode.SPECTATOR); event.getPlayer().setRespawnPoint(new Pos(0, finalY, 0)); }); MinecraftServer.getSchedulerManager().buildShutdownTask(() -> { connectionManager.getOnlinePlayers().forEach(player -> { // TODO: Saving player.kick("Server is closing."); connectionManager.removePlayer(player.getPlayerConnection()); }); OpenToLAN.close(); }); // Preload chunks long start = System.nanoTime(); int radius = MinecraftServer.getChunkViewDistance(); int total = radius * 2 * radius * 2; Loading.start("Preloading " + total + " chunks"); CompletableFuture[] chunkFutures = new CompletableFuture[total]; AtomicInteger completed = new AtomicInteger(0); for (int x = -radius; x < radius; x++) { for (int z = -radius; z < radius; z++) { int index = (x + radius) + (z + radius) * radius; chunkFutures[index] = overworld.loadChunk(x, z) .thenRun(() -> { int completedCount = completed.incrementAndGet(); Loading.updater().progress((double) completedCount / (double) chunkFutures.length); }); } } for (CompletableFuture future : chunkFutures) { if (future != null) future.join(); } long end = System.nanoTime(); Loading.finish(); Logger.debug("Chunks per second: " + (total / ((end - start) / 1e9))); // Debug if (List.of(args).contains("-debug")) { Logger.info("Debug mode enabled."); Logger.info("To disable it, remove the -debug argument"); VanillaDebug.hook(this); } } private ServerProperties getOrGenerateServerProperties() { // TODO: Load from file correctly try { return new ServerProperties(""" #Minecraft server properties from a fresh 1.16.1 server #Generated on Mon Jul 13 17:23:48 CEST 2020 spawn-protection=16 max-tick-time=60000 query.port=25565 generator-settings= sync-chunk-writes=true force-gamemode=false allow-nether=true enforce-whitelist=false gamemode=survival broadcast-console-to-ops=true enable-query=false player-idle-timeout=0 difficulty=easy broadcast-rcon-to-ops=true spawn-monsters=true op-permission-level=4 pvp=true entity-broadcast-range-percentage=100 snooper-enabled=true level-type=default enable-status=true hardcore=false enable-command-block=false max-players=20 network-compression-threshold=256 max-world-size=29999984 resource-pack-sha1= function-permission-level=2 rcon.port=25575 server-port=25565 server-ip= spawn-npcs=true allow-flight=false level-name=world view-distance=10 resource-pack= spawn-animals=true white-list=false rcon.password= generate-structures=true online-mode=false max-build-height=256 level-seed= prevent-proxy-connections=false use-native-transport=true enable-jmx-monitoring=false motd=A Minecraft Server enable-rcon=false """); } catch (Throwable e) { e.printStackTrace(); System.exit(1); return null; } } public void start(String address, int port) { minecraftServer.start(address, port); } public VanillaReimplementation vri() { return vri; } public Instance overworld() { return overworld; } } ================================================ FILE: settings.gradle.kts ================================================ rootProject.name = "VanillaReimplementation" include("core") include("world-generation") include("commands") include("instance-meta") include("block-update-system") include("fluid-simulation") include("item-placeables") include("blocks") include("entities") include("entity-meta") include("server") include("items") include("mojang-data") include("crafting") include("datapack-loading") include("datapack-tests") include("survival") include("datapack") include("loot-table") pluginManagement { repositories { mavenCentral() maven("https://repo.spongepowered.org/repository/maven-public") maven("https://repo.spongepowered.org/repository/maven-snapshots") } } ================================================ FILE: survival/build.gradle.kts ================================================ dependencies { api(project(":core")) api(project(":datapack")) api(project(":loot-table")) api(project(":crafting")) } ================================================ FILE: survival/src/main/java/net/minestom/vanilla/survival/Survival.java ================================================ package net.minestom.vanilla.survival; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.component.DataComponents; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.ItemEntity; import net.minestom.server.entity.Player; import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.LightingChunk; import net.minestom.server.instance.WorldBorder; import net.minestom.server.instance.anvil.AnvilLoader; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.component.EnchantmentList; import net.minestom.server.item.enchant.Enchantment; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.crafting.CraftingFeature; import net.minestom.vanilla.crafting.Recipe; import net.minestom.vanilla.logging.Logger; import net.minestom.vanilla.loot.LootFeature; import net.minestom.vanilla.loot.LootTable; import org.jetbrains.annotations.NotNull; import java.nio.file.Path; import java.time.Duration; import java.util.Map; public class Survival { public static void main(String[] args) { // Initialize the server MinecraftServer minecraftServer = MinecraftServer.init(); // Pass it to survival and initialize new Survival(MinecraftServer.process()).initialize(); Logger.info("Initialized vanilla."); minecraftServer.start("0.0.0.0", 25565); } private final @NotNull ServerProcess process; private final @NotNull InstanceContainer overworld; Survival(@NotNull ServerProcess process) { this.process = process; this.overworld = process.instance().createInstanceContainer(DimensionType.OVERWORLD); this.overworld.setChunkLoader(new AnvilLoader(Path.of("world"))); this.overworld.setChunkSupplier(LightingChunk::new); this.overworld.setWorldBorder(new WorldBorder( 32 * 16 * 2, 0, 0, 10, 500, 560 )); } /** * Initializes the server (event handlers, etc.). */ public void initialize() { Map tables = LootFeature.buildFromDatapack(process); process.eventHandler().addChild(LootFeature.createEventNode(tables)); Map recipes = CraftingFeature.buildFromDatapack(process); process.eventHandler().addChild(CraftingFeature.createEventNode(recipes, process)); process.eventHandler().addListener(AsyncPlayerConfigurationEvent.class, event -> { final Player player = event.getPlayer(); // TODO: Determine respawn coordinates and radius, and then randomly pick a valid spot Pos respawnPoint = new Pos(0, 64, 0, 0, 0); event.setSpawningInstance(this.overworld); player.setRespawnPoint(respawnPoint); var enchs = new EnchantmentList(Map.of( Enchantment.EFFICIENCY, 5, Enchantment.FORTUNE, 3 )); player.getInventory().addItemStack(ItemStack.of(Material.DIAMOND_PICKAXE).with(DataComponents.ENCHANTMENTS, enchs)); player.getInventory().addItemStack(ItemStack.of(Material.DIAMOND_HOE).with(DataComponents.ENCHANTMENTS, enchs)); }).addListener(PlayerSpawnEvent.class, event -> { final Player player = event.getPlayer(); if (event.isFirstSpawn()) { this.broadcast(Component.translatable("multiplayer.player.joined", NamedTextColor.YELLOW).arguments(player.getName())); } }).addListener(PlayerDisconnectEvent.class, event -> { final Player player = event.getPlayer(); this.broadcast(Component.translatable("multiplayer.player.left", NamedTextColor.YELLOW).arguments(player.getName())); }).addListener(PickupItemEvent.class, event -> { if (!(event.getLivingEntity() instanceof Player player)) return; // Cancel event if player does not have enough inventory space ItemStack itemStack = event.getItemEntity().getItemStack(); event.setCancelled(!player.getInventory().addItemStack(itemStack)); }).addListener(ItemDropEvent.class, event -> { final Player player = event.getPlayer(); ItemStack droppedItem = event.getItemStack(); Pos playerPos = player.getPosition(); ItemEntity itemEntity = new ItemEntity(droppedItem); itemEntity.setPickupDelay(Duration.of(500, TimeUnit.MILLISECOND)); itemEntity.setInstance(player.getInstance(), playerPos.withY(y -> y + 1.5)); Vec velocity = playerPos.direction().mul(6); itemEntity.setVelocity(velocity); }); } private void broadcast(@NotNull Component message) { for (Instance instance : process.instance().getInstances()) { instance.sendMessage(message); } } } ================================================ FILE: world-generation/build.gradle.kts ================================================ dependencies { compileOnly(project(":core")) compileOnly(project(":datapack-loading")) } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/Aquifer.java ================================================ package net.minestom.vanilla.generation; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.util.Util; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; import java.util.function.DoubleSupplier; public interface Aquifer { @Nullable Block compute(DensityFunction.Context context, double density); record FluidStatus(int level, Block type) { public Block at(int level) { return level < this.level ? this.type : Block.AIR; } } interface FluidPicker { FluidStatus pickFluid(int x, int y, int z); } static Aquifer createDisabled(FluidPicker fluidPicker) { return (context, density) -> { if (density > 0) { return null; } return fluidPicker.pickFluid(context.blockX(), context.blockY(), context.blockZ()).at(context.blockY()); }; } class NoiseAquifer implements Aquifer { private static final int X_SPACING = 16; private static final int Y_SPACING = 12; private static final int Z_SPACING = 16; private static final int[][] SURFACE_SAMPLING = new int[][]{ {-2, -1}, {-1, -1}, {0, -1}, {1, -1}, {-3, 0}, {-2, 0}, {-1, 0}, {0, 0}, {1, 0}, {-2, 1}, {-1, 1}, {0, 1}, {1, 1}}; private final int minGridX; private final int minGridY; private final int minGridZ; private final int gridSizeX; private final int gridSizeZ; private final int gridSize; private final Map aquiferCache; private final Map aquiferLocationCache; private final NoiseChunk noiseChunk; private final NoiseSettings.NoiseRouter router; private final WorldgenRandom.Positional random; private final FluidPicker globalFluidPicker; public NoiseAquifer( NoiseChunk noiseChunk, Point chunkPos, NoiseSettings.NoiseRouter router, WorldgenRandom.Positional random, int minY, int height, FluidPicker globalFluidPicker) { this.noiseChunk = noiseChunk; this.router = router; this.random = random; this.globalFluidPicker = globalFluidPicker; this.minGridX = this.gridX(Util.chunkMinX(chunkPos)) - 1; this.gridSizeX = this.gridX(Util.chunkMaxX(chunkPos)) + 1 - this.minGridX + 1; this.minGridY = this.gridY(minY) - 1; this.minGridZ = this.gridZ(Util.chunkMinZ(chunkPos)) - 1; this.gridSizeZ = this.gridZ(Util.chunkMaxZ(chunkPos)) + 1 - this.minGridZ + 1; int gridSizeY = this.gridY(minY + height) + 1 - this.minGridY + 1; this.gridSize = this.gridSizeX * gridSizeY * this.gridSizeZ; this.aquiferCache = new HashMap<>(); this.aquiferLocationCache = new HashMap<>(); } public Block compute(DensityFunction.Context context, double density) { int x = context.blockX(); int y = context.blockY(); int z = context.blockZ(); if (density <= 0) { if (this.globalFluidPicker.pickFluid(x, y, z).at(y).compare(Block.LAVA)) { return Block.LAVA; } int gridX = this.gridX(x - 5); int gridY = this.gridY(y + 1); int gridZ = this.gridZ(z - 5); double mag1 = Integer.MAX_VALUE; double mag2 = Integer.MAX_VALUE; double mag3 = Integer.MAX_VALUE; Point loc1 = Vec.ZERO; Point loc2 = Vec.ZERO; Point loc3 = Vec.ZERO; for (int xOffset = 0; xOffset <= 1; xOffset += 1) { for (int yOffset = -1; yOffset <= 1; yOffset += 1) { for (int zOffset = 0; zOffset <= 1; zOffset += 1) { Point location = this.getLocation(gridX + xOffset, gridY + yOffset, gridZ + zOffset); double magnitude = location.distanceSquared(Vec.ZERO); if (mag1 >= magnitude) { loc3 = loc2; loc2 = loc1; loc1 = location; mag3 = mag2; mag2 = mag1; mag1 = magnitude; } else if (mag2 >= magnitude) { loc3 = loc2; loc2 = location; mag3 = mag2; mag2 = magnitude; } else if (mag3 >= magnitude) { loc3 = location; mag3 = magnitude; } } } } FluidStatus status1 = this.getStatus(context, loc1); FluidStatus status2 = this.getStatus(context, loc2); FluidStatus status3 = this.getStatus(context, loc3); double similarity12 = NoiseAquifer.similarity(mag1, mag2); double similarity13 = NoiseAquifer.similarity(mag1, mag3); double similarity23 = NoiseAquifer.similarity(mag2, mag3); double pressure; if (status1.at(y).compare(Block.WATER) && this.globalFluidPicker.pickFluid(x, y - 1, z).at(y - 1).compare(Block.LAVA)) { pressure = 1; } else if (similarity12 > -1) { DoubleSupplier barrier = Util.lazyDouble(() -> this.router.barrier().compute(DensityFunction.context(x, y * 0.5, z))); double pressure12 = this.calculatePressure(y, status1, status2, barrier); double pressure13 = this.calculatePressure(y, status1, status3, barrier); double pressure23 = this.calculatePressure(y, status2, status3, barrier); double n = Math.max(Math.max(pressure12, pressure13 * Math.max(0, similarity13)), pressure23 * similarity23); pressure = Math.max(0, 2 * Math.max(0, similarity12) * n); } else { pressure = 0; } if (density + pressure <= 0) { return status1.at(y); } } return null; } private static double similarity(double a, double b) { return 1 - Math.abs(b - a) / 25; } private double calculatePressure(int y, FluidStatus status1, FluidStatus status2, DoubleSupplier barrier) { Block fluid1 = status1.at(y); Block fluid2 = status2.at(y); if ((fluid1.compare(Block.LAVA) && fluid2.compare(Block.WATER)) || (fluid1.compare(Block.WATER) && fluid2.compare(Block.LAVA))) { return 1; } int levelDiff = Math.abs(status1.level - status2.level); if (levelDiff == 0) { return 0; } double levelAvg = (status1.level + status2.level) / 2.0; double levelAvgDiff = y + 0.5 - levelAvg; double p = levelDiff / 2.0 - Math.abs(levelAvgDiff); double pressure = levelAvgDiff > 0 ? p > 0 ? p / 1.5 : p / 2.5 : p > -3 ? (p + 3) / 3 : (p + 3) / 10; if (pressure < -2 || pressure > 2) { return pressure; } return pressure + barrier.getAsDouble(); } private FluidStatus getStatus(DensityFunction.Context context, Point location) { int x = location.blockX(); int y = location.blockY(); int z = location.blockZ(); int index = this.getIndex(this.gridX(x), this.gridY(y), this.gridZ(z)); FluidStatus cachedStatus = this.aquiferCache.get(index); if (cachedStatus != null) { return cachedStatus; } FluidStatus status = this.computeStatus(context, x, y, z); this.aquiferCache.put(index, status); return status; } private FluidStatus computeStatus(DensityFunction.Context context, int x, int y, int z) { FluidStatus globalStatus = this.globalFluidPicker.pickFluid(x, y, z); int minPreliminarySurface = Integer.MIN_VALUE; boolean isAquifer = false; for (int[] offset : NoiseAquifer.SURFACE_SAMPLING) { int xOffset = offset[0]; int zOffset = offset[1]; int blockX = x + (xOffset << 4); int blockZ = z + (zOffset << 4); int preliminarySurface = this.noiseChunk.getPreliminarySurfaceLevel(blockX, blockZ); minPreliminarySurface = Math.min(minPreliminarySurface, preliminarySurface); boolean noOffset = xOffset == 0 && zOffset == 0; if (noOffset && y - 12 > preliminarySurface + 8) { return globalStatus; } if ((noOffset || y + 12 > preliminarySurface + 8)) { FluidStatus newStatus = this.globalFluidPicker.pickFluid(blockX, preliminarySurface + 8, blockZ); if (!newStatus.at(preliminarySurface + 8).compare(Block.AIR)) { if (noOffset) { return newStatus; } else { isAquifer = true; } } } } double allowedFloodedness = isAquifer ? Util.clampedMap(minPreliminarySurface + 8 - y, 0, 64, 1, 0) : 0; double floodedness = Util.clamp(this.router.fluid_level_floodedness().compute(DensityFunction.context(x, y * 0.67, z)), -1, 1); if (floodedness > Util.map(allowedFloodedness, 1, 0, -0.3, 0.8)) { return globalStatus; } if (floodedness <= Util.map(allowedFloodedness, 1, 0, -0.8, 0.4)) { return new FluidStatus(Integer.MIN_VALUE, globalStatus.type); } int gridY = (int) Math.floor(y / 40); double spread = this.router.fluid_level_spread().compute(DensityFunction.context(Math.floor(x / 16), gridY, Math.floor(z / 16))); int level = gridY * 40 + 20 + (int) Math.floor(spread / 3) * 3; int statusLevel = Math.min(minPreliminarySurface, level); Block fluid = this.getFluidType(context, x, y, z, globalStatus.type, level); return new FluidStatus(statusLevel, fluid); } private Block getFluidType(DensityFunction.Context context, double x, double y, double z, Block global, int level) { if (level <= -10) { double lava = this.router.lava().compute(DensityFunction.context(Math.floor(x / 64), Math.floor(y / 40), Math.floor(z / 64))); if (Math.abs(lava) > 0.3) { return Block.LAVA; } } return global; } private Point getLocation(int x, int y, int z) { int index = this.getIndex(x, y, z); Point cachedLocation = this.aquiferLocationCache.get(index); if (Vec.ZERO.equals(cachedLocation)) { return cachedLocation; } WorldgenRandom random = this.random.at(x, y, z); Point location = new Vec( x * NoiseAquifer.X_SPACING + random.nextInt(10), y * NoiseAquifer.Y_SPACING + random.nextInt(9), z * NoiseAquifer.Z_SPACING + random.nextInt(10)); this.aquiferLocationCache.put(index, location); return location; } private int getIndex(int x, int y, int z) { int gridX = x - this.minGridX; int gridY = y - this.minGridY; int gridZ = z - this.minGridZ; int index = (gridY * this.gridSizeZ + gridZ) * this.gridSizeX + gridX; if (index < 0 || index >= this.gridSize) { throw new Error("Invalid aquifer index at (" + x + ", " + y + ", " + z + ") : 0 <= " + index + " < " + gridSize); } return index; } private int gridX(int x) { return (int) Math.floor(x / NoiseAquifer.X_SPACING); } private int gridY(int y) { return (int) Math.floor(y / NoiseAquifer.Y_SPACING); } private int gridZ(int z) { return (int) Math.floor(z / NoiseAquifer.Z_SPACING); } } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/NoiseChunk.java ================================================ package net.minestom.vanilla.generation; import net.minestom.server.coordinate.CoordConversion; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.List; import java.util.Map; public class NoiseChunk { public final int cellWidth; public final int cellHeight; public final int firstCellX; public final int firstCellZ; public final double firstNoiseX; public final double firstNoiseZ; public final double noiseSizeXZ; private final Map preliminarySurfaceLevel = new HashMap<>(); private final Aquifer aquifer; private final MaterialRule materialRule; private final DensityFunction initialDensity; public int cellCountXZ; public int cellCountY; public int cellNoiseMinY; public int minX; public int minZ; public NoiseSettings settings; public NoiseChunk( int cellCountXZ, int cellCountY, int cellNoiseMinY, RandomState randomState, int minX, int minZ, NoiseSettings settings, boolean aquifersEnabled, Aquifer.FluidPicker fluidPicker) { this.cellWidth = NoiseSettings.cellWidth(settings); this.cellHeight = NoiseSettings.cellHeight(settings); this.firstCellX = (int) (double) (minX / this.cellWidth); this.firstCellZ = (int) (double) (minZ / this.cellWidth); this.firstNoiseX = minX >> 2; this.firstNoiseZ = minZ >> 2; this.noiseSizeXZ = (cellCountXZ * this.cellWidth) >> 2; if (true) { // WIP: Noise aquifers don't work yet this.aquifer = Aquifer.createDisabled(fluidPicker); } else { Point chunkPos = new Vec(minX, 0, minZ); int minY = cellNoiseMinY * NoiseSettings.cellHeight(settings); int height = cellCountY * NoiseSettings.cellHeight(settings); this.aquifer = new Aquifer.NoiseAquifer(this, chunkPos, randomState.router, randomState.aquiferRandom, minY, height, fluidPicker); } DensityFunction finalDensity = randomState.router.final_density(); this.materialRule = MaterialRule.fromList(List.of( (context) -> this.aquifer.compute(context, finalDensity.compute(context)) )); this.initialDensity = randomState.router.initial_density_without_jaggedness(); } public @Nullable Block getFinalState(Datapack datapack, int x, int y, int z) { return this.materialRule.compute(DensityFunction.context(x, y, z)); } public int getPreliminarySurfaceLevel(int quartX, int quartZ) { return preliminarySurfaceLevel.computeIfAbsent(CoordConversion.chunkIndex(quartX, quartZ), (key) -> { int x = quartX << 2; int z = quartZ << 2; for (int y = this.settings.noise().min_y() + this.settings.noise().height(); y >= this.settings.noise().min_y(); y -= this.cellHeight) { double density = this.initialDensity.compute(DensityFunction.context(x, y, z)); if (density > 0.390625) { return y; } } return Integer.MIN_VALUE; }); } public interface MaterialRule { @Nullable Block compute(DensityFunction.Context context); static MaterialRule fromList(List rules) { return (context) -> { for (MaterialRule rule : rules) { Block state = rule.compute(context); if (state != null) return state; } return null; }; } } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/NoiseChunkGenerator.java ================================================ package net.minestom.vanilla.generation; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.CoordConversion; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.batch.ChunkBatch; import net.minestom.server.instance.block.Block; import net.minestom.server.world.DimensionType; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.WorldgenContext; import net.minestom.vanilla.datapack.worldgen.biome.BiomeSource; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnknownNullability; import java.util.HashMap; import java.util.Map; public class NoiseChunkGenerator { private final Map noiseChunkCache = new HashMap<>(); private final Aquifer.FluidPicker globalFluidPicker; // constructor( // private readonly biomeSource: BiomeSource, // private readonly settings: NoiseGeneratorSettings, // ) { // this.noiseChunkCache = new Map() // // const lavaFluid = new FluidStatus(-54, BlockState.LAVA) // const defaultFluid = new FluidStatus(settings.seaLevel, settings.defaultFluid) // this.globalFluidPicker = (x, y, z) => { // if (y < Math.min(-54, settings.seaLevel)) { // return lavaFluid // } // return defaultFluid // } // } private final @NotNull Datapack datapack; private final @NotNull BiomeSource biomeSource; private final @NotNull NoiseSettings settings; // Minestom private final DimensionType dimensionType; public NoiseChunkGenerator(@NotNull Datapack datapack, @NotNull BiomeSource biomeSource, @NotNull NoiseSettings settings, DimensionType dimensionType) { this.datapack = datapack; this.biomeSource = biomeSource; this.settings = settings; this.dimensionType = dimensionType; Aquifer.FluidStatus lavaFluid = new Aquifer.FluidStatus(-54, Block.LAVA); Aquifer.FluidStatus defaultFluid = new Aquifer.FluidStatus(settings.sea_level(), settings.default_fluid().toMinestom()); this.globalFluidPicker = (x, y, z) -> { if (y < Math.min(-54, settings.sea_level())) { return lavaFluid; } return defaultFluid; }; } // public fill(randomState: RandomState, chunk: Chunk, onlyFirstZ: boolean = false) { // const minY = Math.max(chunk.minY, this.settings.noise.minY) // const maxY = Math.min(chunk.maxY, this.settings.noise.minY + this.settings.noise.height) // // const cellWidth = NoiseSettings.cellWidth(this.settings.noise) // const cellHeight = NoiseSettings.cellHeight(this.settings.noise) // const cellCountXZ = Math.floor(16 / cellWidth) // // const minCellY = Math.floor(minY / cellHeight) // const cellCountY = Math.floor((maxY - minY) / cellHeight) // // const minX = ChunkPos.minBlockX(chunk.pos) // const minZ = ChunkPos.minBlockZ(chunk.pos) // // const noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk) public void fill(Datapack datapack, RandomState randomState, TargetChunk chunk) { fill(datapack, randomState, chunk, false); } public void fill(Datapack datapack, RandomState randomState, TargetChunk chunk, boolean onlyFirstZ) { int minY = Math.max(chunk.minY(), this.settings.noise().min_y()); int maxY = Math.min(chunk.maxY(), this.settings.noise().min_y() + this.settings.noise().height()); int cellWidth = NoiseSettings.cellWidth(this.settings); int cellHeight = NoiseSettings.cellHeight(this.settings); int cellCountXZ = Math.floorDiv(16, cellWidth); int minCellY = Math.floorDiv(minY, cellHeight); int cellCountY = Math.floorDiv(maxY - minY, cellHeight); NoiseChunk noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk); for (int cellX = 0; cellX < cellCountXZ; cellX += 1) { for (int cellZ = 0; cellZ < (onlyFirstZ ? 1 : cellCountXZ); cellZ += 1) { for (int cellY = cellCountY - 1; cellY >= 0; cellY -= 1) { for (int offY = cellHeight - 1; offY >= 0; offY -= 1) { int blockY = (minCellY + cellY) * cellHeight + offY; int sectionY = blockY / Chunk.CHUNK_SECTION_SIZE; for (int offX = 0; offX < cellWidth; offX += 1) { int blockX = chunk.minX() + cellX * cellWidth + offX; int sectionX = blockX & 0xF; for (int offZ = 0; offZ < (onlyFirstZ ? 1 : cellWidth); offZ += 1) { int blockZ = chunk.minZ() + cellZ * cellWidth + offZ; int sectionZ = blockZ & 0xF; Block state = noiseChunk.getFinalState(datapack, blockX, blockY, blockZ); if (state == null) { state = this.settings.default_block().toMinestom(); } chunk.setBlock(blockX, blockY, blockZ, state); } } } } } } } // public buildSurface(randomState: RandomState, chunk: Chunk, /** @deprecated */ biome: string = 'minecraft:plains') { // const noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk) // const context = WorldgenContext.create(this.settings.noise.minY, this.settings.noise.height) // randomState.surfaceSystem.buildSurface(chunk, noiseChunk, context, () => biome) // } public void buildSurface(Datapack datapack, RandomState randomState, TargetChunk chunk, Key biome) { NoiseChunk noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk); WorldgenContext context = WorldgenContext.create(this.dimensionType); randomState.surfaceSystem.buildSurface(chunk, noiseChunk, context, point -> biome); } public Key computeBiome(RandomState randomState, int quartX, int quartY, int quartZ) { return this.biomeSource.getBiome(quartX, quartY, quartZ, randomState.sampler); } private NoiseChunk getOrCreateNoiseChunk(RandomState randomState, TargetChunk chunk) { return this.noiseChunkCache.computeIfAbsent(chunk.index(), ignored -> { // const minY = Math.max(chunk.minY, this.settings.noise.minY) // const maxY = Math.min(chunk.maxY, this.settings.noise.minY + this.settings.noise.height) // // const cellWidth = NoiseSettings.cellWidth(this.settings.noise) // const cellHeight = NoiseSettings.cellHeight(this.settings.noise) // const cellCountXZ = Math.floor(16 / cellWidth) // // const minCellY = Math.floor(minY / cellHeight) // const cellCountY = Math.floor((maxY - minY) / cellHeight) // const minX = ChunkPos.minBlockX(chunk.pos) // const minZ = ChunkPos.minBlockZ(chunk.pos) // // return new NoiseChunk(cellCountXZ, cellCountY, minCellY, randomState, minX, minZ, this.settings.noise, this.settings.aquifersEnabled, this.globalFluidPicker) int minY = Math.max(chunk.minY(), this.settings.noise().min_y()); int maxY = Math.min(chunk.maxY(), this.settings.noise().min_y() + this.settings.noise().height()); int cellWidth = NoiseSettings.cellWidth(this.settings); int cellHeight = NoiseSettings.cellHeight(this.settings); int cellCountXZ = Math.floorDiv(Chunk.CHUNK_SECTION_SIZE, cellWidth); int minCellY = Math.floorDiv(minY, cellHeight); int cellCountY = Math.floorDiv(maxY - minY, cellHeight); int minX = chunk.minX(); int minZ = chunk.minZ(); return new NoiseChunk(cellCountXZ, cellCountY, minCellY, randomState, minX, minZ, this.settings, this.settings.aquifers_enabled(), this.globalFluidPicker); }); } public synchronized void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) { TargetChunkImpl chunk = new TargetChunkImpl(batch, chunkX, chunkZ, dimensionType.minY() / Chunk.CHUNK_SECTION_SIZE, dimensionType.maxY() / Chunk.CHUNK_SECTION_SIZE); RandomState randomState = new RandomState(settings, 125); fill(this.datapack, randomState, chunk); } private static class TargetChunkImpl implements TargetChunk { private final int chunkX; private final int chunkZ; private final int minSection; private final int maxSection; private final ChunkBatch batch; private final Int2ObjectMap blocks = new Int2ObjectOpenHashMap<>(); public TargetChunkImpl(ChunkBatch batch, int chunkX, int chunkZ, int minSection, int maxSection) { this.chunkX = chunkX; this.chunkZ = chunkZ; this.minSection = minSection; this.maxSection = maxSection; this.batch = batch; } @Override public int chunkX() { return this.chunkX; } @Override public int chunkZ() { return this.chunkZ; } @Override public int minSection() { return this.minSection; } @Override public int maxSection() { return this.maxSection; } @Override public @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) { int index = CoordConversion.chunkBlockIndex(x, y, z); return this.blocks.getOrDefault(index, Block.STONE); } @Override public void setBlock(int x, int y, int z, @NotNull Block block) { if (x < minX() || x >= maxX() || y < minY() || y >= maxY() || z < minZ() || z >= maxZ()) { return; } int index = CoordConversion.chunkBlockIndex(x, y, z); this.blocks.put(index, block); batch.setBlock(x - minX(), y, z - minZ(), block); } } public interface TargetChunk extends Block.Getter, Block.Setter { int chunkX(); int chunkZ(); default int minX() { return chunkX() * Chunk.CHUNK_SIZE_X; } default int maxX() { return minX() + Chunk.CHUNK_SIZE_X; } default int minZ() { return chunkZ() * Chunk.CHUNK_SIZE_Z; } default int maxZ() { return minZ() + Chunk.CHUNK_SIZE_Z; } default long index() { return CoordConversion.chunkIndex(chunkX(), chunkZ()); } int minSection(); int maxSection(); default int minY() { return minSection() * Chunk.CHUNK_SECTION_SIZE; } default int maxY() { return (maxSection() + 1) * Chunk.CHUNK_SECTION_SIZE; } } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/RandomState.java ================================================ package net.minestom.vanilla.generation; import net.kyori.adventure.key.Key; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.biome.Climate; import net.minestom.vanilla.datapack.worldgen.random.LegacyRandom; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.random.XoroshiroRandom; public class RandomState { public final WorldgenRandom.Positional random; public final WorldgenRandom.Positional aquiferRandom; public final WorldgenRandom.Positional oreRandom; public final SurfaceSystem surfaceSystem; public final NoiseSettings.NoiseRouter router; public final Climate.Sampler sampler; public final long seed; public RandomState(NoiseSettings settings, long seed) { this.seed = seed; this.random = (settings.legacy_random_source() ? new LegacyRandom(seed) : new XoroshiroRandom(seed)).forkPositional(); this.aquiferRandom = this.random.fromHashOf(Key.key("aquifer").toString()).forkPositional(); this.oreRandom = this.random.fromHashOf(Key.key("ore").toString()).forkPositional(); this.surfaceSystem = new SurfaceSystem(settings.surface_rule(), settings.default_block().toMinestom(), seed); this.router = settings.noise_router(); this.sampler = Climate.Sampler.fromRouter(this.router); } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/SurfaceContext.java ================================================ package net.minestom.vanilla.generation; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.world.biome.Biome; import net.minestom.vanilla.datapack.worldgen.DensityFunction; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.WorldgenContext; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.util.Util; import java.util.function.Function; import java.util.function.IntSupplier; import java.util.function.Supplier; public class SurfaceContext implements NoiseSettings.SurfaceRule.Context { public int blockX; public int blockY; public int blockZ; public int stoneDepthAbove; public int stoneDepthBelow; public int surfaceDepth; public int waterHeight; public Supplier fetchBiome = Biome.PLAINS::key; public IntSupplier surfaceSecondary = () -> 0; public IntSupplier minSurfaceLevel = () -> 0; public final SurfaceSystem system; public final NoiseChunkGenerator.TargetChunk chunk; public final NoiseChunk noiseChunk; public final WorldgenContext context; private final Function getBiome; public SurfaceContext(SurfaceSystem system, NoiseChunkGenerator.TargetChunk chunk, NoiseChunk noiseChunk, WorldgenContext context, Function getBiome) { this.system = system; this.chunk = chunk; this.noiseChunk = noiseChunk; this.context = context; this.getBiome = getBiome; } public void updateXZ(int x, int z) { this.blockX = x; this.blockZ = z; this.surfaceDepth = this.system.getSurfaceDepth(x, z); this.surfaceSecondary = Util.lazyInt(() -> (int) this.system.getSurfaceSecondary(x, z)); this.minSurfaceLevel = Util.lazyInt(() -> this.calculateMinSurfaceLevel(x, z)); } public void updateY(int stoneDepthAbove, int stoneDepthBelow, int waterHeight, int y) { this.blockY = y; this.stoneDepthAbove = stoneDepthAbove; this.stoneDepthBelow = stoneDepthBelow; this.waterHeight = waterHeight; this.fetchBiome = Util.lazy(() -> this.getBiome.apply(new Vec(this.blockX, this.blockY, this.blockZ))); } private int calculateMinSurfaceLevel(int x, int z) { int cellX = x >> 4; int cellZ = z >> 4; int level00 = this.noiseChunk.getPreliminarySurfaceLevel(cellX << 4, cellZ << 4); int level10 = this.noiseChunk.getPreliminarySurfaceLevel((cellX + 1) << 4, cellZ << 4); int level01 = this.noiseChunk.getPreliminarySurfaceLevel(cellX << 4, (cellZ + 1) << 4); int level11 = this.noiseChunk.getPreliminarySurfaceLevel((cellX + 1) << 4, (cellZ + 1) << 4); int level = (int) Math.floor(Util.lerp2((double) (x & 0xF) / 16, (double) (z & 0xF) / 16, level00, level10, level01, level11)); return level + this.surfaceDepth - 8; } private DensityFunction.Context asDFContext() { return DensityFunction.context(this.blockX, this.blockY, this.blockZ); } @Override public Key biome() { return this.fetchBiome.get(); } @Override public int minY() { return this.blockY - this.stoneDepthAbove; } @Override public int maxY() { return context.minY(); } @Override public int blockX() { return this.blockX; } @Override public int blockY() { return this.blockY; } @Override public int blockZ() { return this.blockZ; } @Override public WorldgenRandom random(String string) { return this.system.getRandom(string); } @Override public int stoneDepthAbove() { return this.stoneDepthAbove; } @Override public int surfaceDepth() { return this.surfaceDepth; } @Override public int waterHeight() { return this.waterHeight; } @Override public int minSurfaceLevel() { return this.minSurfaceLevel.getAsInt(); } @Override public int stoneDepthBelow() { return this.stoneDepthBelow; } @Override public double surfaceSecondary() { return this.surfaceSecondary.getAsInt(); } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/SurfaceSystem.java ================================================ package net.minestom.vanilla.generation; import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.block.Block; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.datapack.worldgen.WorldgenContext; import net.minestom.vanilla.datapack.worldgen.WorldgenRegistries; import net.minestom.vanilla.datapack.worldgen.noise.NormalNoise; import net.minestom.vanilla.datapack.worldgen.random.WorldgenRandom; import net.minestom.vanilla.datapack.worldgen.random.XoroshiroRandom; import java.util.HashMap; import java.util.Map; import java.util.function.Function; public class SurfaceSystem { private final NormalNoise surfaceNoise; private final NormalNoise surfaceSecondaryNoise; private final WorldgenRandom.Positional random; private final Map positionalRandoms; private final NoiseSettings.SurfaceRule rule; private final Block defaultBlock; public SurfaceSystem(NoiseSettings.SurfaceRule rule, Block defaultBlock, long seed) { this.random = new XoroshiroRandom(seed).forkPositional(); this.surfaceNoise = NoiseSettings.NoiseRouter.instantiate(this.random, WorldgenRegistries.SURFACE_NOISE); this.surfaceSecondaryNoise = NoiseSettings.NoiseRouter.instantiate(this.random, WorldgenRegistries.SURFACE_SECONDARY_NOISE); this.positionalRandoms = new HashMap<>(); this.rule = rule; this.defaultBlock = defaultBlock; } public void buildSurface(NoiseChunkGenerator.TargetChunk chunk, NoiseChunk noiseChunk, WorldgenContext context, Function getBiome) { int minX = chunk.minX(); int minZ = chunk.minZ(); int minY = chunk.minY(); int maxY = chunk.maxY(); SurfaceContext surfaceContext = new SurfaceContext(this, chunk, noiseChunk, context, getBiome); var ruleWithContext = this.rule.apply(surfaceContext); for (int x = 0; x < Chunk.CHUNK_SIZE_X; x += 1) { int worldX = minX + x; for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z += 1) { int worldZ = minZ + z; surfaceContext.updateXZ(worldX, worldZ); int stoneDepthAbove = 0; int waterHeight = Integer.MIN_VALUE; int stoneDepthOffset = Integer.MAX_VALUE; for (int y = minY; y >= maxY; y -= 1) { var worldPos = new Vec(worldX, y, worldZ); var oldState = chunk.getBlock(worldPos); if (oldState.compare(Block.AIR)) { stoneDepthAbove = 0; waterHeight = Integer.MIN_VALUE; continue; } if (oldState.registry().isLiquid()) { if (waterHeight == Integer.MIN_VALUE) { waterHeight = y + 1; } continue; } if (stoneDepthOffset >= y) { stoneDepthOffset = Integer.MIN_VALUE; for (int i = y - 1; i >= minY; i -= 1) { Block state = chunk.getBlock(new Vec(worldX, i, worldZ)); if (state.compare(Block.AIR) || state.registry().isLiquid()) { stoneDepthOffset = i + 1; break; } } } stoneDepthAbove += 1; int stoneDepthBelow = y - stoneDepthOffset + 1; if (!oldState.equals(this.defaultBlock)) { continue; } surfaceContext.updateY(stoneDepthAbove, stoneDepthBelow, waterHeight, y); var newState = ruleWithContext.apply(worldX, y, worldZ); if (newState != null) { chunk.setBlock(worldPos, newState); } } } } } public int getSurfaceDepth(double x, double z) { double noise = this.surfaceNoise.sample(x, 0, z); double offset = this.random.at((int) x, 0, (int) z).nextDouble() * 0.25; return (int) (noise * 2.75 + 3 + offset); } public double getSurfaceSecondary(double x, double z) { return this.surfaceSecondaryNoise.sample(x, 0, z); } public WorldgenRandom getRandom(String name) { return positionalRandoms.computeIfAbsent(name, this.random::fromHashOf); } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/VanillaTestGenerator.java ================================================ package net.minestom.vanilla.generation; import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; import de.articdive.jnoise.generators.noisegen.white.WhiteNoiseGenerator; import de.articdive.jnoise.pipeline.JNoise; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.generator.GenerationUnit; import net.minestom.server.instance.generator.Generator; import net.minestom.server.world.biome.Biome; import org.jetbrains.annotations.NotNull; public class VanillaTestGenerator implements Generator { private final JNoise noise = JNoise.newBuilder() .fastSimplex(FastSimplexNoiseGenerator.newBuilder() .build()) .scale(1.0 / 32.0) .build(); private final JNoise treeNoise = JNoise.newBuilder() .white(WhiteNoiseGenerator.newBuilder().build()) .build(); private synchronized double noise(JNoise noise, double x, double z) { return noise.evaluateNoise(x, 0, z); } @Override public void generate(@NotNull GenerationUnit unit) { var modifier = unit.modifier(); modifier.fillBiome(Biome.PLAINS); Point start = unit.absoluteStart(); Point end = unit.absoluteEnd(); for (int x = start.blockX(); x < end.blockX(); x++) { for (int z = start.blockZ(); z < end.blockZ(); z++) { double heightDelta = noise(noise, x, z); int height = (int) (64 - heightDelta * 16); int bottom = 0; int stone = height; int dirt = stone + 5; int grass = dirt + 1; for (int y = bottom; y < stone; y++) { modifier.setBlock(x, y, z, Block.STONE); } for (int y = stone; y < dirt; y++) { modifier.setBlock(x, y, z, Block.DIRT); } for (int y = dirt; y < grass; y++) { modifier.setBlock(x, y, z, Block.GRASS_BLOCK); } if (height < 64) { // Too low for a tree // However we can put water here for (int y = height; y < 64; y++) { modifier.setBlock(x, y, z, Block.WATER); } continue; } if (noise(treeNoise, x, z) > 0.9) { Point treePos = new Vec(x, grass, z); unit.fork(setter -> spawnTree(setter, treePos)); } } } } private void spawnTree(Block.Setter setter, Point pos) { int trunkX = pos.blockX(); int trunkBottomY = pos.blockY(); int trunkZ = pos.blockZ(); for (int i = 0; i < 2; i++) { setter.setBlock(trunkX + 1, trunkBottomY + 3 + i, trunkZ, Block.OAK_LEAVES); setter.setBlock(trunkX - 1, trunkBottomY + 3 + i, trunkZ, Block.OAK_LEAVES); setter.setBlock(trunkX, trunkBottomY + 3 + i, trunkZ + 1, Block.OAK_LEAVES); setter.setBlock(trunkX, trunkBottomY + 3 + i, trunkZ - 1, Block.OAK_LEAVES); for (int x = -1; x <= 1; x++) { for (int z = -1; z <= 1; z++) { setter.setBlock(trunkX + x, trunkBottomY + 2 + i, trunkZ - z, Block.OAK_LEAVES); } } } setter.setBlock(trunkX, trunkBottomY, trunkZ, Block.OAK_LOG); setter.setBlock(trunkX, trunkBottomY + 1, trunkZ, Block.OAK_LOG); setter.setBlock(trunkX, trunkBottomY + 2, trunkZ, Block.OAK_LOG); setter.setBlock(trunkX, trunkBottomY + 3, trunkZ, Block.OAK_LOG); setter.setBlock(trunkX, trunkBottomY + 4, trunkZ, Block.OAK_LEAVES); } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/VanillaWorldGenerationFeature.java ================================================ package net.minestom.vanilla.generation; import net.kyori.adventure.key.Key; import net.minestom.vanilla.VanillaReimplementation; import net.minestom.vanilla.datapack.Datapack; import net.minestom.vanilla.datapack.DatapackLoadingFeature; import net.minestom.vanilla.datapack.worldgen.NoiseSettings; import net.minestom.vanilla.instance.SetupVanillaInstanceEvent; import org.jetbrains.annotations.NotNull; import java.util.Set; public class VanillaWorldGenerationFeature implements VanillaReimplementation.Feature { @Override public void hook(@NotNull HookContext context) { context.vri().process().eventHandler().addListener(SetupVanillaInstanceEvent.class, event -> { Key plains = Key.key("minecraft:plains"); DatapackLoadingFeature datapackLoading = context.vri().feature(DatapackLoadingFeature.class); Datapack datapack = datapackLoading.current(); Datapack.NamespacedData data = datapack.namespacedData().get("minecraft"); if (data == null) { throw new IllegalStateException("minecraft namespace not found"); } NoiseSettings settings = data.world_gen().noise_settings().file("overworld.json"); // BiomeSource.fromJson() // ThreadLocal generators = ThreadLocal.withInitial(() -> new NoiseChunkGenerator(datapack, (x, y, z, sampler) -> plains, settings, event.getInstance().getDimensionType())); // event.getInstance().setChunkGenerator(new ChunkGenerator() { // @Override // public void generateChunkData(@NotNull ChunkBatch batch, int chunkX, int chunkZ) { // generators.get().generateChunkData(batch, chunkX, chunkZ); // } // // @Override // public @Nullable List getPopulators() { // return null; // } // }); }); } @Override public @NotNull Key key() { return Key.key("vri:worldgeneration"); } @Override public @NotNull Set> dependencies() { return Set.of(DatapackLoadingFeature.class); } } ================================================ FILE: world-generation/src/main/java/net/minestom/vanilla/generation/VanillaWorldgen.java ================================================ package net.minestom.vanilla.generation; public final class VanillaWorldgen { // TODO: Vanila worldgen }