From 9ddfdf9374333737e94804b6f53cd95849b3471c Mon Sep 17 00:00:00 2001 From: Kas-tle <26531652+Kas-tle@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:04:08 -0700 Subject: [PATCH] Add support for custom blocks and skulls (#3505) * Super cursed custom skulls custom block * Rename some stuff * Attempt to clean up some code * Remove skull translation events and define custom blocks for custom skulls Clean up skull block translation a bit * Auto generate skull resource pack Change `davchoo` to `geyser` in geometry * Add config options for custom blocks and custom skull blocks * Fix formatting and names for player skulls * Use block states more efficiently for custom skulls 21 block states vs 48 block states * Clean up custom block api a bit * Apply some suggestions from Camotoy * Move custom skull config stuff to its own file Custom skulls can now be added by username, uuid, and textures Move skull nbt stuff from requestTexturesFromUsername to SkullBlockEntityTranslator Add requestTexturesFromUUID * Update custom block nbt for v534 * Disable collision box & selection box when box is empty Fix incorrect collision names used in CustomBlockComponentsBuilder * Add custom block stuff to provider registry loader * More API changes Convert CustomBlockPermutation into a record Change materialInstances in CustomBlockComponents Builder to materialInstance Reuse box components in CustomSkull * Convert skull floor geometries into a template Should be easier to modify in needed in the future. * Crop and reorder skull textures to eliminate unused space Should reduce memory & storage usage for Bedrock clients * Revert "Crop and reorder skull textures to eliminate unused space" This reverts commit 15fd5353e1c2b8bcffd6d5986095a3646a6e7bf9. * Use identifier from CustomBlockData in SkullResourcePackManager * Fix isIncorrectHeldItem check for custom skull blocks Add defaultBlockState to CustomBlockData * Fix adding duplicate block states for custom blocks with 0 properties Remove defaultBlockState CustomBlockState field from GeyserCustomBlockData since it creates a circular reference * Add basis for overriding Bedrock block states Fix missing providers when used in GeyserDefineCustomBlocksEvent * Fix custom blocks in 1.19.50 * Decouple mappings from items * Decouple mappings from items * Null check * Move to CustomBlockRegistryPopulator * Remove name_hash from blocksTag/vanillaBlockStates Fixes creative inventory contents with custom blocks registered * Limit Bedrock versions to 1.19.40+ Custom blocks were released in 1.19.40 * Un-revert Crop and reorder skull textures to eliminate unused space Should reduce memory & storage usage for Bedrock clients Bug with top face flipping + per-face uv's was fixed in 1.19.40+ https://bugs.mojang.com/browse/MCPE-160073 Geometry is still offset by 0.5 to prevent lighting bugs * Add validation custom block components and s/lightFilter/lightDampening/ Also validate custom block names * Add display name component and add toggle for client block placing The display name component allows blocks to use other locale keys. placeAir will prevent the client from placing the default block state. * Begin parsing block mappings (still much to do!) * CustomBlockMapping stores block w/ all states * Mappings almost :/ * Ok now they work at least * Read most mapping components * Block mappings mostly done * Translate block item * Add docs for custom blocks * Add tags * More docs * Accidentally added name comp. * Fix collide box and warn for >16 props * add registerBlockItemOverride event + refactor * Inventory overrides for multistate bedrock blocks * Implement all remaining block components * Minor cleanup and javadocs * Update custom skull config example * Address @Camotoy's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix light_emission and light_dampening components * Remove redundant populate method and remove BLOCKS_JSON after last use * Fix inventories with block state overrides not opening * API event for skull blocks & let register via URL Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Use skin hash instead of URL Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @davchoo's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Rework MappingsReader_v1 to avoid passing maps around * Treat all properties as string properties There isn't a real need to check for boolean and int properties * Fix block registry scan in MappingsReader * Skin hashes can have less than 64 characters? * Include entry when logging exceptions from block mappings * Submodule Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix block break speeds thanks to @Camotoy Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Temporarily fix build on eclipse so I may work... Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Custom tool breakspeed by server; Closes #3348 Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Account for if custom skulls are added on 1st run Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Initial framework for extended collision boxes Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Add some notes for the extended collision box impl Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * We have our extended collision registry Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Notes for me Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Extended collision boxes almost work Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Extended collision boxes actually work Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Consider all hitboxes in calculation Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * X is mirrored... Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Extended collision boxes are much improved Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Upstream fallout Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @Redned235's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Oops my bad that makes no sense :) Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Ext collision box chunk translation optimization Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Trunc skinhash to 32 chars due to 80 char limit Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Use new transformation cmpnt vs legacy rotation Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * keep arr null on get extcolstor Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Properly handle if extended collision box is below Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Less ugly (realized it can go here) Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Prevent 2x placement due to extended collision box Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Properly build on eclipse via indra Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Ensure enough bits in bedrockData for paletteIDs Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix not needed whitespace Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Update license headers to 2023 Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Use release indra over snapshot Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Revert "Update license headers to 2023" This reverts commit f750059e8ee01dfd4e9f9d13d3bdb85eb90e1f74. * Account for collisions in chunk section y0 layer Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix extended collision @ air section bottom Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @davchoo's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @rtm516's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * More @rtm516's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @Camotoy's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Update javadocs Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @davchoo's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Lock extended collision to section Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Clear ext col even when air Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Let override vanilla items in creative inventory Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Avoid creating 12 HashSets for every overrided block state * Super minor nitpicks + Custom Skull NBT fix * Check custom skull is within Bedrock bounds Fixes NPE with custom skulls above y=320 or below y=-64 * Add static builder methods to match CustomItemData API * Upstream Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Initial API setup for modded blocks (no impl yet) Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * More work on nonvanilla blocks (nonfunctional) Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix compile Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Update submodules Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Modded reg so far (not done) Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Add non-vanilla registration and fix a few bugs * Fixes for non-vanilla blocks * Remove import * CustomRegPop. go1st for now; must split for modded Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address silent change to geo component for blocks Co-Authored-By: Unoqwy Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Seperate bedrock, vanilla, & nonvanilla block reg Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Single event Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Impl MaterialInstance as builder per @Redned235 Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Added creative category enum & added some missing overrides (#7) * Add material instance to provider registry Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * oops Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix case of correctBedrockIdentifier not found Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix docs Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address @Camotoy's review Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address review from @davchoo Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Set namespace of custom blocks vs ident direct Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Address review from @rtm516 Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * One more Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Remove rogue space * Geo component as builder Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * use super name Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Bump version Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> --------- Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> Signed-off-by: GitHub Co-authored-by: davchoo Co-authored-by: davchoo <4722249+davchoo@users.noreply.github.com> Co-authored-by: Unoqwy Co-authored-by: RednedEpic Co-authored-by: ImDaBigBoss <67973871+ImDaBigBoss@users.noreply.github.com> Co-authored-by: rtm516 --- .../api/block/custom/CustomBlockData.java | 143 ++++ .../block/custom/CustomBlockPermutation.java | 39 + .../api/block/custom/CustomBlockState.java | 75 ++ .../custom/NonVanillaCustomBlockData.java | 91 +++ .../block/custom/component/BoxComponent.java | 69 ++ .../component/CustomBlockComponents.java | 192 +++++ .../custom/component/GeometryComponent.java | 69 ++ .../custom/component/MaterialInstance.java | 84 +++ .../custom/component/PlacementConditions.java | 53 ++ .../component/TransformationComponent.java | 67 ++ .../custom/nonvanilla/JavaBlockItem.java | 7 + .../custom/nonvanilla/JavaBlockState.java | 111 +++ .../custom/nonvanilla/JavaBoundingBox.java | 6 + .../custom/property/CustomBlockProperty.java | 56 ++ .../block/custom/property/PropertyType.java | 79 ++ .../GeyserDefineCustomBlocksEvent.java | 76 ++ .../GeyserDefineCustomSkullsEvent.java | 28 + .../geyser/api/util/CreativeCategory.java | 66 ++ build-logic/build.gradle.kts | 2 +- .../kotlin/geyser.base-conventions.gradle.kts | 2 +- build.gradle.kts | 12 +- .../java/org/geysermc/geyser/Constants.java | 4 + .../GeyserCustomSkullConfiguration.java | 65 ++ .../holder/BlockInventoryHolder.java | 19 +- .../mappings/versions/MappingsReader_v1.java | 132 ---- .../geyser/item/type/PlayerHeadItem.java | 35 +- .../block/GeyserCustomBlockComponents.java | 305 ++++++++ .../level/block/GeyserCustomBlockData.java | 217 ++++++ .../block/GeyserCustomBlockProperty.java | 44 ++ .../level/block/GeyserCustomBlockState.java | 112 +++ .../level/block/GeyserGeometryComponent.java | 76 ++ .../level/block/GeyserJavaBlockState.java | 161 ++++ .../level/block/GeyserMaterialInstance.java | 102 +++ .../GeyserNonVanillaCustomBlockData.java | 118 +++ .../geyser/pack/SkullResourcePackManager.java | 306 ++++++++ .../geyser/registry/BlockRegistries.java | 60 +- .../geysermc/geyser/registry/Registries.java | 18 +- .../loader/CollisionRegistryLoader.java | 5 + .../loader/ProviderRegistryLoader.java | 20 + .../registry/loader/ResourcePackLoader.java | 7 + .../mappings/MappingsConfigReader.java | 76 +- .../util/CustomBlockComponentsMapping.java | 40 + .../mappings/util/CustomBlockMapping.java | 44 ++ .../util/CustomBlockStateBuilderMapping.java | 42 ++ .../util/CustomBlockStateMapping.java | 40 + .../mappings/versions/MappingsReader.java | 7 +- .../mappings/versions/MappingsReader_v1.java | 713 ++++++++++++++++++ .../populator/BlockRegistryPopulator.java | 242 +++++- .../CustomBlockRegistryPopulator.java | 480 ++++++++++++ .../CustomItemRegistryPopulator.java | 72 +- .../CustomSkullRegistryPopulator.java | 187 +++++ .../populator/ItemRegistryPopulator.java | 101 ++- .../geyser/registry/type/BlockMapping.java | 1 + .../geyser/registry/type/BlockMappings.java | 18 + .../geyser/registry/type/CustomSkull.java | 166 ++++ .../registry/type/GeyserMappingItem.java | 2 + .../geyser/registry/type/ItemMappings.java | 4 + .../geyser/session/GeyserSession.java | 17 + .../geyser/session/cache/SkullCache.java | 97 ++- .../org/geysermc/geyser/skin/SkinManager.java | 2 +- .../geysermc/geyser/skin/SkinProvider.java | 11 +- .../chest/DoubleChestInventoryTranslator.java | 44 +- .../inventory/item/ItemTranslator.java | 91 ++- .../level/block/entity/PistonBlockEntity.java | 6 +- .../entity/SkullBlockEntityTranslator.java | 53 +- ...BedrockInventoryTransactionTranslator.java | 36 +- .../player/BedrockActionTranslator.java | 50 +- .../java/JavaUpdateRecipesTranslator.java | 6 +- .../level/JavaBlockEntityDataTranslator.java | 19 +- .../JavaLevelChunkWithLightTranslator.java | 209 ++++- .../org/geysermc/geyser/util/BlockUtils.java | 4 +- .../org/geysermc/geyser/util/ChunkUtils.java | 44 +- .../org/geysermc/geyser/util/FileUtils.java | 9 + .../org/geysermc/geyser/util/MathUtils.java | 48 ++ .../animations/disable.animation.json | 14 + .../animations/player_skull.animation.json | 80 ++ .../attachables/template_attachable.json | 57 ++ .../bedrock/skull_resource_pack/manifest.json | 17 + .../models/blocks/player_skull.geo.json | 73 ++ .../models/blocks/player_skull_floor.geo.json | 53 ++ .../models/blocks/player_skull_hand.geo.json | 52 ++ .../models/blocks/player_skull_wall.geo.json | 52 ++ .../textures/terrain_texture.json | 8 + .../bedrock/skull_resource_pack_files.txt | 7 + core/src/main/resources/config.yml | 3 +- core/src/main/resources/custom-skulls.yml | 29 + .../src/main/resources/icon.png | Bin gradle.properties | 4 +- 88 files changed, 6235 insertions(+), 328 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java create mode 100644 core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java delete mode 100644 core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java rename core/src/main/java/org/geysermc/geyser/{item => registry}/mappings/MappingsConfigReader.java (58%) create mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java rename core/src/main/java/org/geysermc/geyser/{item => registry}/mappings/versions/MappingsReader.java (86%) create mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/animations/disable.animation.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/animations/player_skull.animation.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/attachables/template_attachable.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/manifest.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/models/blocks/player_skull.geo.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/models/blocks/player_skull_floor.geo.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/models/blocks/player_skull_hand.geo.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/models/blocks/player_skull_wall.geo.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack/textures/terrain_texture.json create mode 100644 core/src/main/resources/bedrock/skull_resource_pack_files.txt create mode 100644 core/src/main/resources/custom-skulls.yml rename {bootstrap/standalone => core}/src/main/resources/icon.png (100%) diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java new file mode 100644 index 000000000..9f142faab --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; +import java.util.Map; + +/** + * This class is used to store data for a custom block. + */ +public interface CustomBlockData { + /** + * Gets the name of the custom block + * + * @return The name of the custom block. + */ + @NonNull String name(); + + /** + * Gets the identifier of the custom block + * + * @return The identifier of the custom block. + */ + @NonNull String identifier(); + + /** + * Gets if the custom block is included in the creative inventory + * + * @return If the custom block is included in the creative inventory. + */ + boolean includedInCreativeInventory(); + + /** + * Gets the item's creative category, or tab id. + * + * @return the item's creative category + */ + @Nullable CreativeCategory creativeCategory(); + + /** + * Gets the item's creative group. + * + * @return the item's creative group + */ + @Nullable String creativeGroup(); + + /** + * Gets the components of the custom block + * + * @return The components of the custom block. + */ + @Nullable CustomBlockComponents components(); + + /** + * Gets the custom block's map of block property names to CustomBlockProperty + * objects + * + * @return The custom block's map of block property names to CustomBlockProperty objects. + */ + @NonNull Map> properties(); + + /** + * Gets the list of the custom block's permutations + * + * @return The permutations of the custom block. + */ + @NonNull List permutations(); + + /** + * Gets the custom block's default block state + * + * @return The default block state of the custom block. + */ + @NonNull CustomBlockState defaultBlockState(); + + /** + * Gets a builder for a custom block state + * + * @return The builder for a custom block state. + */ + CustomBlockState.@NonNull Builder blockStateBuilder(); + + /** + * Create a Builder for CustomBlockData + * + * @return A CustomBlockData Builder + */ + static CustomBlockData.Builder builder() { + return GeyserApi.api().provider(CustomBlockData.Builder.class); + } + + interface Builder { + Builder name(@NonNull String name); + + Builder includedInCreativeInventory(boolean includedInCreativeInventory); + + Builder creativeCategory(@Nullable CreativeCategory creativeCategory); + + Builder creativeGroup(@Nullable String creativeGroup); + + Builder components(@NonNull CustomBlockComponents components); + + Builder booleanProperty(@NonNull String propertyName); + + Builder intProperty(@NonNull String propertyName, List values); + + Builder stringProperty(@NonNull String propertyName, List values); + + Builder permutations(@NonNull List permutations); + + CustomBlockData build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java new file mode 100644 index 000000000..fca39b533 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; + +/** + * This class is used to store a custom block permutations, which contain custom + * block components mapped to a Molang query that should return true or false + * + * @param components The components of the block + * @param condition The Molang query that should return true or false + */ +public record CustomBlockPermutation(@NonNull CustomBlockComponents components, @NonNull String condition) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java new file mode 100644 index 000000000..70b3c1e2d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Map; + +/** + * This class is used to store a custom block state, which contains CustomBlockData + * tied to defined properties and values + */ +public interface CustomBlockState { + /** + * Gets the custom block data associated with the state + * + * @return The custom block data for the state. + */ + @NonNull CustomBlockData block(); + + /** + * Gets the name of the state + * + * @return The name of the state. + */ + @NonNull String name(); + + /** + * Gets the given property for the state + * + * @param propertyName the property name + * @return the boolean, int, or string property. + */ + @NonNull T property(@NonNull String propertyName); + + /** + * Gets a map of the properties for the state + * + * @return The properties for the state. + */ + @NonNull Map properties(); + + interface Builder { + Builder booleanProperty(@NonNull String propertyName, boolean value); + + Builder intProperty(@NonNull String propertyName, int value); + + Builder stringProperty(@NonNull String propertyName, @NonNull String value); + + CustomBlockState build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java new file mode 100644 index 000000000..4ab186ce6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; + +/** + * Represents a completely custom block that is not based on an existing vanilla Minecraft block. + */ +public interface NonVanillaCustomBlockData extends CustomBlockData { + /** + * Gets the namespace of the custom block + * + * @return The namespace of the custom block. + */ + @NonNull String namespace(); + + + /** + * Create a Builder for NonVanillaCustomBlockData + * + * @return A NonVanillaCustomBlockData Builder + */ + static NonVanillaCustomBlockData.Builder builder() { + return GeyserApi.api().provider(NonVanillaCustomBlockData.Builder.class); + } + + interface Builder extends CustomBlockData.Builder { + + Builder namespace(@NonNull String namespace); + + @Override + Builder name(@NonNull String name); + + @Override + Builder includedInCreativeInventory(boolean includedInCreativeInventory); + + @Override + Builder creativeCategory(@Nullable CreativeCategory creativeCategory); + + @Override + Builder creativeGroup(@Nullable String creativeGroup); + + @Override + Builder components(@NonNull CustomBlockComponents components); + + @Override + Builder booleanProperty(@NonNull String propertyName); + + @Override + Builder intProperty(@NonNull String propertyName, List values); + + @Override + Builder stringProperty(@NonNull String propertyName, List values); + + @Override + Builder permutations(@NonNull List permutations); + + @Override + NonVanillaCustomBlockData build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java new file mode 100644 index 000000000..0b577742d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +/** + * This class is used to store a box component for the selection and + * collision boxes of a custom block. + * + * @param originX The origin X of the box + * @param originY The origin Y of the box + * @param originZ The origin Z of the box + * @param sizeX The size X of the box + * @param sizeY The size Y of the box + * @param sizeZ The size Z of the box + */ +public record BoxComponent(float originX, float originY, float originZ, float sizeX, float sizeY, float sizeZ) { + private static final BoxComponent FULL_BOX = new BoxComponent(-8, 0, -8, 16, 16, 16); + private static final BoxComponent EMPTY_BOX = new BoxComponent(0, 0, 0, 0, 0, 0); + + /** + * Gets a full box component + * + * @return A full box component + */ + public static BoxComponent fullBox() { + return FULL_BOX; + } + + /** + * Gets an empty box component + * + * @return An empty box component + */ + public static BoxComponent emptyBox() { + return EMPTY_BOX; + } + + /** + * Gets if the box component is empty + * + * @return If the box component is empty. + */ + public boolean isEmpty() { + return sizeX == 0 && sizeY == 0 && sizeZ == 0; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java new file mode 100644 index 000000000..036723092 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class is used to store components for a custom block or custom block permutation. + */ +public interface CustomBlockComponents { + + /** + * Gets the selection box component + * Equivalent to "minecraft:selection_box" + * + * @return The selection box. + */ + @Nullable BoxComponent selectionBox(); + + /** + * Gets the collision box component + * Equivalent to "minecraft:collision_box" + * @return The collision box. + */ + @Nullable BoxComponent collisionBox(); + + /** + * Gets the display name component + * Equivalent to "minecraft:display_name" + * + * @return The display name. + */ + @Nullable String displayName(); + + /** + * Gets the geometry component + * Equivalent to "minecraft:geometry" + * + * @return The geometry. + */ + @Nullable GeometryComponent geometry(); + + /** + * Gets the material instances component + * Equivalent to "minecraft:material_instances" + * + * @return The material instances. + */ + @NonNull Map materialInstances(); + + /** + * Gets the placement filter component + * Equivalent to "minecraft:placement_filter" + * + * @return The placement filter. + */ + @Nullable List placementFilter(); + + /** + * Gets the destructible by mining component + * Equivalent to "minecraft:destructible_by_mining" + * + * @return The destructible by mining value. + */ + @Nullable Float destructibleByMining(); + + /** + * Gets the friction component + * Equivalent to "minecraft:friction" + * + * @return The friction value. + */ + @Nullable Float friction(); + + /** + * Gets the light emission component + * Equivalent to "minecraft:light_emission" + * + * @return The light emission value. + */ + @Nullable Integer lightEmission(); + + /** + * Gets the light dampening component + * Equivalent to "minecraft:light_dampening" + * + * @return The light dampening value. + */ + @Nullable Integer lightDampening(); + + /** + * Gets the transformation component + * Equivalent to "minecraft:transformation" + * + * @return The transformation. + */ + @Nullable TransformationComponent transformation(); + + /** + * Gets the unit cube component + * Equivalent to "minecraft:unit_cube" + * + * @return The rotation. + */ + boolean unitCube(); + + /** + * Gets if the block should place only air + * Equivalent to setting a dummy event to run on "minecraft:on_player_placing" + * + * @return If the block should place only air. + */ + boolean placeAir(); + + /** + * Gets the set of tags + * Equivalent to "tag:some_tag" + * + * @return The set of tags. + */ + @NonNull Set tags(); + + /** + * Create a Builder for CustomBlockComponents + * + * @return A CustomBlockComponents Builder + */ + static CustomBlockComponents.Builder builder() { + return GeyserApi.api().provider(CustomBlockComponents.Builder.class); + } + + interface Builder { + Builder selectionBox(BoxComponent selectionBox); + + Builder collisionBox(BoxComponent collisionBox); + + Builder displayName(String displayName); + + Builder geometry(GeometryComponent geometry); + + Builder materialInstance(@NonNull String name, @NonNull MaterialInstance materialInstance); + + Builder placementFilter(List placementConditions); + + Builder destructibleByMining(Float destructibleByMining); + + Builder friction(Float friction); + + Builder lightEmission(Integer lightEmission); + + Builder lightDampening(Integer lightDampening); + + Builder transformation(TransformationComponent transformation); + + Builder unitCube(boolean unitCube); + + Builder placeAir(boolean placeAir); + + Builder tags(Set tags); + + CustomBlockComponents build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java new file mode 100644 index 000000000..6d85989a7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.Map; + +/** + * This class is used to store data for a geometry component. + */ +public interface GeometryComponent { + + /** + * Gets the identifier of the geometry + * + * @return The identifier of the geometry. + */ + @NonNull String identifier(); + + /** + * Gets the bone visibility of the geometry + * + * @return The bone visibility of the geometry. + */ + @Nullable Map boneVisibility(); + + /** + * Creates a builder for GeometryComponent + * + * @return a builder for GeometryComponent. + */ + static GeometryComponent.Builder builder() { + return GeyserApi.api().provider(GeometryComponent.Builder.class); + } + + interface Builder { + Builder identifier(@NonNull String identifier); + + Builder boneVisibility(@Nullable Map boneVisibility); + + GeometryComponent build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java new file mode 100644 index 000000000..26edab2f6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +/** + * This class is used to store data for a material instance. + */ +public interface MaterialInstance { + /** + * Gets the texture of the block + * + * @return The texture of the block. + */ + @NonNull String texture(); + + /** + * Gets the render method of the block + * + * @return The render method of the block. + */ + @Nullable String renderMethod(); + + /** + * Gets if the block should be dimmed on certain faces + * + * @return If the block should be dimmed on certain faces. + */ + @Nullable boolean faceDimming(); + + /** + * Gets if the block should have ambient occlusion + * + * @return If the block should have ambient occlusion. + */ + @Nullable boolean ambientOcclusion(); + + /** + * Creates a builder for MaterialInstance. + * + * @return a builder for MaterialInstance + */ + static MaterialInstance.Builder builder() { + return GeyserApi.api().provider(MaterialInstance.Builder.class); + } + + interface Builder { + Builder texture(@NonNull String texture); + + Builder renderMethod(@Nullable String renderMethod); + + Builder faceDimming(@Nullable boolean faceDimming); + + Builder ambientOcclusion(@Nullable boolean ambientOcclusion); + + MaterialInstance build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java new file mode 100644 index 000000000..d06d9a967 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser +*/ + +package org.geysermc.geyser.api.block.custom.component; + +import java.util.LinkedHashMap; +import java.util.Set; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * This class is used to store conditions for a placement filter for a custom block. + * + * @param allowedFaces The faces that the block can be placed on + * @param blockFilters The block filters that control what blocks the block can be placed on + */ +public record PlacementConditions(@NonNull Set allowedFaces, @NonNull LinkedHashMap blockFilters) { + public enum Face { + DOWN, + UP, + NORTH, + SOUTH, + WEST, + EAST; + } + + public enum BlockFilterType { + BLOCK, + TAG + } +} \ No newline at end of file diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java new file mode 100644 index 000000000..d52565e22 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +/** + * This class is used to store the transformation component of a block + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + * @param sx The scale on the x axis + * @param sy The scale on the y axis + * @param sz The scale on the z axis + * @param tx The translation on the x axis + * @param ty The translation on the y axis + * @param tz The translation on the z axis + */ +public record TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz, float tx, float ty, float tz) { + + /** + * Constructs a new TransformationComponent with the rotation values and assumes default scale and translation + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + */ + public TransformationComponent(int rx, int ry, int rz) { + this(rx, ry, rz, 1, 1, 1, 0, 0, 0); + } + + /** + * Constructs a new TransformationComponent with the rotation and scale values and assumes default translation + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + * @param sx The scale on the x axis + * @param sy The scale on the y axis + * @param sz The scale on the z axis + */ + public TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz) { + this(rx, ry, rz, sx, sy, sz, 0, 0, 0); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java new file mode 100644 index 000000000..5143246d9 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java @@ -0,0 +1,7 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record JavaBlockItem(@NonNull String identifier, @NonNegative int javaId, @NonNegative int stackSize) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java new file mode 100644 index 000000000..6293506a8 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java @@ -0,0 +1,111 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +public interface JavaBlockState { + /** + * Gets the identifier of the block state + * + * @return the identifier of the block state + */ + @NonNull String identifier(); + + /** + * Gets the Java ID of the block state + * + * @return the Java ID of the block state + */ + @NonNegative int javaId(); + + /** + * Gets the state group ID of the block state + * + * @return the state group ID of the block state + */ + @NonNegative int stateGroupId(); + + /** + * Gets the block hardness of the block state + * + * @return the block hardness of the block state + */ + @NonNegative float blockHardness(); + + /** + * Gets whether the block state is waterlogged + * + * @return whether the block state is waterlogged + */ + @NonNull boolean waterlogged(); + + /** + * Gets the collision of the block state + * + * @return the collision of the block state + */ + @NonNull JavaBoundingBox[] collision(); + + /** + * Gets whether the block state can be broken with hand + * + * @return whether the block state can be broken with hand + */ + @NonNull boolean canBreakWithHand(); + + /** + * Gets the pick item of the block state + * + * @return the pick item of the block state + */ + @Nullable String pickItem(); + + /** + * Gets the piston behavior of the block state + * + * @return the piston behavior of the block state + */ + @Nullable String pistonBehavior(); + + /** + * Gets whether the block state has block entity + * + * @return whether the block state has block entity + */ + @Nullable boolean hasBlockEntity(); + + /** + * Creates a new {@link JavaBlockState.Builder} instance + * + * @return a new {@link JavaBlockState.Builder} instance + */ + static JavaBlockState.Builder builder() { + return GeyserApi.api().provider(JavaBlockState.Builder.class); + } + + interface Builder { + Builder identifier(@NonNull String identifier); + + Builder javaId(@NonNegative int javaId); + + Builder stateGroupId(@NonNegative int stateGroupId); + + Builder blockHardness(@NonNegative float blockHardness); + + Builder waterlogged(@NonNull boolean waterlogged); + + Builder collision(@NonNull JavaBoundingBox[] collision); + + Builder canBreakWithHand(@NonNull boolean canBreakWithHand); + + Builder pickItem(@Nullable String pickItem); + + Builder pistonBehavior(@Nullable String pistonBehavior); + + Builder hasBlockEntity(@Nullable boolean hasBlockEntity); + + JavaBlockState build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java new file mode 100644 index 000000000..56a4ca3da --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java @@ -0,0 +1,6 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public record JavaBoundingBox(@NonNull double middleX, @NonNull double middleY, @NonNull double middleZ, @NonNull double sizeX, @NonNull double sizeY, @NonNull double sizeZ) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java new file mode 100644 index 000000000..fc39c4663 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.property; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; + +/** + * This class is used to store a property of a custom block of a generic type. + */ +public interface CustomBlockProperty { + /** + * Gets the name of the property + * + * @return The name of the property. + */ + @NonNull String name(); + + /** + * Gets the values of the property + * + * @return The values of the property. + */ + @NonNull List values(); + + /** + * Gets the type of the property + * + * @return The type of the property. + */ + @NonNull PropertyType type(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java new file mode 100644 index 000000000..48f25c36c --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.property; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * This class is used to define a custom block property's type. + */ +public class PropertyType { + private static final PropertyType BOOLEAN = new PropertyType(Boolean.class); + private static final PropertyType INTEGER = new PropertyType(Integer.class); + private static final PropertyType STRING = new PropertyType(String.class); + + /** + * Gets the property type for a boolean. + * + * @return The property type for a boolean. + */ + @NonNull public static PropertyType booleanProp() { + return BOOLEAN; + } + + /** + * Gets the property type for an integer. + * + * @return The property type for an integer. + */ + @NonNull public static PropertyType integerProp() { + return INTEGER; + } + + /** + * Gets the property type for a string. + * + * @return The property type for a string. + */ + @NonNull public static PropertyType stringProp() { + return STRING; + } + + private final Class typeClass; + + /** + * Gets the class of the property type + * + * @return The class of the property type. + */ + @NonNull public Class typeClass() { + return typeClass; + } + + private PropertyType(Class typeClass) { + this.typeClass = typeClass; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java new file mode 100644 index 000000000..b1a01d7e6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.event.Event; + +/** + * Called on Geyser's startup when looking for custom blocks. Custom blocks must be registered through this event. + * + * This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config. + */ +public abstract class GeyserDefineCustomBlocksEvent implements Event { + /** + * Registers the given {@link CustomBlockData} as a custom block + * + * @param customBlockData the custom block to register + */ + public abstract void register(@NonNull CustomBlockData customBlockData); + + /** + * Registers the given {@link CustomBlockState} as an override for the + * given java state identifier + * Java state identifiers are listed in + * https://raw.githubusercontent.com/GeyserMC/mappings/master/blocks.json + * + * @param javaIdentifier the java state identifier to override + * @param customBlockState the custom block state with which to override java state identifier + */ + public abstract void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState); + + /** + * Registers the given {@link CustomBlockData} as an override for the + * given java item identifier + * + * @param javaIdentifier the java item identifier to override + * @param customBlockData the custom block data with which to override java item identifier + */ + public abstract void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData); + + /** + * Registers the given {@link CustomBlockState} as an override for the + * given {@link JavaBlockState} + * + * @param javaBlockState the java block state for the non-vanilla block + * @param customBlockState the custom block state with which to override java state identifier + */ + public abstract void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java new file mode 100644 index 000000000..17f7b599a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java @@ -0,0 +1,28 @@ +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Event; + +/** + * Called on Geyser's startup when looking for custom skulls. Custom skulls must be registered through this event. + * + * This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config. + */ +public abstract class GeyserDefineCustomSkullsEvent implements Event { + /** + * The type of texture provided + */ + public enum SkullTextureType { + USERNAME, + UUID, + PROFILE, + SKIN_HASH + } + + /** + * Registers the given username, UUID, base64 encoded profile, or skin hash as a custom skull blocks + * @param texture the username, UUID, base64 encoded profile, or skin hash + * @param type the type of texture provided + */ + public abstract void register(@NonNull String texture, @NonNull SkullTextureType type); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java new file mode 100644 index 000000000..7519ff157 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.util; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents the creative menu categories or tabs. + */ +public enum CreativeCategory { + COMMANDS("commands", 1), + CONSTRUCTION("construction", 2), + EQUIPMENT("equipment", 3), + ITEMS("items", 4), + NATURE("nature", 5), + NONE("none", 6); + + private final String internalName; + private final int id; + + CreativeCategory(String internalName, int id) { + this.internalName = internalName; + this.id = id; + } + + /** + * Gets the internal name of the category. + * + * @return the name of the category + */ + @NonNull public String internalName() { + return internalName; + } + + /** + * Gets the internal ID of the category. + * + * @return the ID of the category + */ + public int id() { + return id; + } +} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 3d1fb47f7..c0c683920 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation("net.kyori", "indra-common", "3.0.1") + implementation("net.kyori", "indra-common", "3.1.1") implementation("com.github.johnrengelman", "shadow", "7.1.3-SNAPSHOT") // Within the gradle plugin classpath, there is a version conflict between loom and some other diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 709867300..93d702939 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -34,4 +34,4 @@ tasks { ) } } -} +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2eed8de91..4cb5b0223 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,9 +5,15 @@ plugins { } allprojects { - group = "org.geysermc.geyser" - version = "2.1.2-SNAPSHOT" - description = "Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers." + group = properties["group"] as String + "." + properties["id"] as String + version = properties["version"] as String + description = properties["description"] as String +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(16)) + } } val platforms = setOf( diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 12e8b0d8c..5de8e6e6b 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -41,6 +41,10 @@ public final class Constants { static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom"; + + public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/"; + static { URI wsUri = null; try { diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java new file mode 100644 index 000000000..1af3578a3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final +public class GeyserCustomSkullConfiguration { + @JsonProperty("player-usernames") + private List playerUsernames; + + @JsonProperty("player-uuids") + private List playerUUIDs; + + @JsonProperty("player-profiles") + private List playerProfiles; + + @JsonProperty("skin-hashes") + private List skinHashes; + + public List getPlayerUsernames() { + return Objects.requireNonNullElse(playerUsernames, Collections.emptyList()); + } + + public List getPlayerUUIDs() { + return Objects.requireNonNullElse(playerUUIDs, Collections.emptyList()); + } + + public List getPlayerProfiles() { + return Objects.requireNonNullElse(playerProfiles, Collections.emptyList()); + } + + public List getPlayerSkinHashes() { + return Objects.requireNonNullElse(skinHashes, Collections.emptyList()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index 01365be34..c81fe6598 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -77,15 +77,18 @@ public class BlockInventoryHolder extends InventoryHolder { // (This could be a virtual inventory that the player is opening) if (checkInteractionPosition(session)) { // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + // and the bedrock block is vanilla int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); - String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); - if (isValidBlock(javaBlockString)) { - // We can safely use this block - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); - setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) { + String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); + if (isValidBlock(javaBlockString)) { + // We can safely use this block + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); - return true; + return true; + } } } @@ -97,7 +100,7 @@ public class BlockInventoryHolder extends InventoryHolder { UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(position); - blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(defaultJavaBlockState)); + blockPacket.setDefinition(session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState)); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); inventory.setHolderPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java b/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java deleted file mode 100644 index e08190aff..000000000 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.item.mappings.versions; - -import com.fasterxml.jackson.databind.JsonNode; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.item.custom.CustomItemData; -import org.geysermc.geyser.api.item.custom.CustomItemOptions; -import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; - -import java.nio.file.Path; -import java.util.function.BiConsumer; - -public class MappingsReader_v1 extends MappingsReader { - @Override - public void readMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - this.readItemMappings(file, mappingsRoot, consumer); - } - - public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - JsonNode itemsNode = mappingsRoot.get("items"); - - if (itemsNode != null && itemsNode.isObject()) { - itemsNode.fields().forEachRemaining(entry -> { - if (entry.getValue().isArray()) { - entry.getValue().forEach(data -> { - try { - CustomItemData customItemData = this.readItemMappingEntry(data); - consumer.accept(entry.getKey(), customItemData); - } catch (InvalidCustomMappingsFileException e) { - GeyserImpl.getInstance().getLogger().error("Error in custom mapping file: " + file.toString(), e); - } - }); - } - }); - } - } - - private CustomItemOptions readItemCustomItemOptions(JsonNode node) { - CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder(); - - JsonNode customModelData = node.get("custom_model_data"); - if (customModelData != null && customModelData.isInt()) { - customItemOptions.customModelData(customModelData.asInt()); - } - - JsonNode damagePredicate = node.get("damage_predicate"); - if (damagePredicate != null && damagePredicate.isInt()) { - customItemOptions.damagePredicate(damagePredicate.asInt()); - } - - JsonNode unbreakable = node.get("unbreakable"); - if (unbreakable != null && unbreakable.isBoolean()) { - customItemOptions.unbreakable(unbreakable.asBoolean()); - } - - JsonNode defaultItem = node.get("default"); - if (defaultItem != null && defaultItem.isBoolean()) { - customItemOptions.defaultItem(defaultItem.asBoolean()); - } - - return customItemOptions.build(); - } - - @Override - public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException { - if (node == null || !node.isObject()) { - throw new InvalidCustomMappingsFileException("Invalid item mappings entry"); - } - - JsonNode name = node.get("name"); - if (name == null || !name.isTextual() || name.asText().isEmpty()) { - throw new InvalidCustomMappingsFileException("An item entry has no name"); - } - - CustomItemData.Builder customItemData = CustomItemData.builder() - .name(name.asText()) - .customItemOptions(this.readItemCustomItemOptions(node)); - - //The next entries are optional - if (node.has("display_name")) { - customItemData.displayName(node.get("display_name").asText()); - } - - if (node.has("icon")) { - customItemData.icon(node.get("icon").asText()); - } - - if (node.has("allow_offhand")) { - customItemData.allowOffhand(node.get("allow_offhand").asBoolean()); - } - - if (node.has("display_handheld")) { - customItemData.displayHandheld(node.get("display_handheld").asBoolean()); - } - - if (node.has("texture_size")) { - customItemData.textureSize(node.get("texture_size").asInt()); - } - - if (node.has("render_offsets")) { - JsonNode tmpNode = node.get("render_offsets"); - - customItemData.renderOffsets(fromJsonNode(tmpNode)); - } - - return customItemData.build(); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java index 5c985c732..662448a52 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; +import org.geysermc.geyser.translator.text.MessageTranslator; public class PlayerHeadItem extends Item { public PlayerHeadItem(String javaIdentifier, Builder builder) { @@ -42,29 +43,35 @@ public class PlayerHeadItem extends Item { public void translateNbtToBedrock(@NonNull GeyserSession session, @NonNull CompoundTag tag) { super.translateNbtToBedrock(session, tag); - Tag display = tag.get("display"); - if (!(display instanceof CompoundTag) || !((CompoundTag) display).contains("Name")) { - Tag skullOwner = tag.get("SkullOwner"); - if (skullOwner != null) { + CompoundTag displayTag; + if (tag.get("display") instanceof CompoundTag existingDisplayTag) { + displayTag = existingDisplayTag; + } else { + displayTag = new CompoundTag("display"); + tag.put(displayTag); + } + + if (displayTag.get("Name") instanceof StringTag nameTag) { + // Custom names are always yellow and italic + displayTag.put(new StringTag("Name", ChatColor.YELLOW + ChatColor.ITALIC + MessageTranslator.convertMessageLenient(nameTag.getValue(), session.locale()))); + } else { + if (tag.contains("SkullOwner")) { StringTag name; - if (skullOwner instanceof StringTag) { - name = (StringTag) skullOwner; + Tag skullOwner = tag.get("SkullOwner"); + if (skullOwner instanceof StringTag skullName) { + name = skullName; } else { - StringTag skullName; - if (skullOwner instanceof CompoundTag && (skullName = ((CompoundTag) skullOwner).get("Name")) != null) { + if (skullOwner instanceof CompoundTag && ((CompoundTag) skullOwner).get("Name") instanceof StringTag skullName) { name = skullName; } else { - session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + tag); + // No name found so default to "Player Head" + displayTag.put(new StringTag("Name", ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale()))); return; } } // Add correct name of player skull - // TODO: It's always yellow, even with a custom name. Handle? String displayName = ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head.named", session.locale()).replace("%s", name.getValue()); - if (!(display instanceof CompoundTag)) { - tag.put(display = new CompoundTag("display")); - } - ((CompoundTag) display).put(new StringTag("Name", displayName)); + displayTag.put(new StringTag("Name", displayName)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java new file mode 100644 index 000000000..e43e168ee --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions; +import org.geysermc.geyser.api.block.custom.component.TransformationComponent; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Value +public class GeyserCustomBlockComponents implements CustomBlockComponents { + BoxComponent selectionBox; + BoxComponent collisionBox; + String displayName; + GeometryComponent geometry; + Map materialInstances; + List placementFilter; + Float destructibleByMining; + Float friction; + Integer lightEmission; + Integer lightDampening; + TransformationComponent transformation; + boolean unitCube; + boolean placeAir; + Set tags; + + private GeyserCustomBlockComponents(CustomBlockComponentsBuilder builder) { + this.selectionBox = builder.selectionBox; + this.collisionBox = builder.collisionBox; + this.displayName = builder.displayName; + this.geometry = builder.geometry; + if (builder.materialInstances.isEmpty()) { + this.materialInstances = Object2ObjectMaps.emptyMap(); + } else { + this.materialInstances = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.materialInstances)); + } + this.placementFilter = builder.placementFilter; + this.destructibleByMining = builder.destructibleByMining; + this.friction = builder.friction; + this.lightEmission = builder.lightEmission; + this.lightDampening = builder.lightDampening; + this.transformation = builder.transformation; + this.unitCube = builder.unitCube; + this.placeAir = builder.placeAir; + if (builder.tags.isEmpty()) { + this.tags = Set.of(); + } else { + this.tags = Set.copyOf(builder.tags); + } + } + + @Override + public BoxComponent selectionBox() { + return selectionBox; + } + + @Override + public BoxComponent collisionBox() { + return collisionBox; + } + + @Override + public String displayName() { + return displayName; + } + + @Override + public GeometryComponent geometry() { + return geometry; + } + + @Override + public @NonNull Map materialInstances() { + return materialInstances; + } + + @Override + public List placementFilter() { + return placementFilter; + } + + @Override + public Float destructibleByMining() { + return destructibleByMining; + } + + @Override + public Float friction() { + return friction; + } + + @Override + public Integer lightEmission() { + return lightEmission; + } + + @Override + public Integer lightDampening() { + return lightDampening; + } + + @Override + public TransformationComponent transformation() { + return transformation; + } + + @Override + public boolean unitCube() { + return unitCube; + } + + @Override + public boolean placeAir() { + return placeAir; + } + + @Override + public @NotNull Set tags() { + return tags; + } + + public static class CustomBlockComponentsBuilder implements Builder { + protected BoxComponent selectionBox; + protected BoxComponent collisionBox; + protected String displayName; + protected GeometryComponent geometry; + protected final Object2ObjectMap materialInstances = new Object2ObjectOpenHashMap<>(); + protected List placementFilter; + protected Float destructibleByMining; + protected Float friction; + protected Integer lightEmission; + protected Integer lightDampening; + protected TransformationComponent transformation; + protected boolean unitCube = false; + protected boolean placeAir = false; + protected final Set tags = new HashSet<>(); + + private void validateBox(BoxComponent box) { + if (box == null) { + return; + } + if (box.sizeX() < 0 || box.sizeY() < 0 || box.sizeZ() < 0) { + throw new IllegalArgumentException("Box size must be non-negative."); + } + float minX = box.originX() + 8; + float minY = box.originY(); + float minZ = box.originZ() + 8; + float maxX = minX + box.sizeX(); + float maxY = minY + box.sizeY(); + float maxZ = minZ + box.sizeZ(); + if (minX < 0 || minY < 0 || minZ < 0 || maxX > 16 || maxY > 16 || maxZ > 16) { + throw new IllegalArgumentException("Box bounds must be within (0, 0, 0) and (16, 16, 16). Recieved: (" + minX + ", " + minY + ", " + minZ + ") to (" + maxX + ", " + maxY + ", " + maxZ + ")"); + } + } + + @Override + public Builder selectionBox(BoxComponent selectionBox) { + validateBox(selectionBox); + this.selectionBox = selectionBox; + return this; + } + + @Override + public Builder collisionBox(BoxComponent collisionBox) { + validateBox(collisionBox); + this.collisionBox = collisionBox; + return this; + } + + @Override + public Builder displayName(String displayName) { + this.displayName = displayName; + return this; + } + + @Override + public Builder geometry(GeometryComponent geometry) { + this.geometry = geometry; + return this; + } + + @Override + public Builder materialInstance(@NotNull String name, @NotNull MaterialInstance materialInstance) { + this.materialInstances.put(name, materialInstance); + return this; + } + + @Override + public Builder placementFilter(List placementFilter) { + this.placementFilter = placementFilter; + return this; + } + + @Override + public Builder destructibleByMining(Float destructibleByMining) { + if (destructibleByMining != null && destructibleByMining < 0) { + throw new IllegalArgumentException("Destructible by mining must be non-negative"); + } + this.destructibleByMining = destructibleByMining; + return this; + } + + @Override + public Builder friction(Float friction) { + if (friction != null) { + if (friction < 0 || friction > 1) { + throw new IllegalArgumentException("Friction must be in the range 0-1"); + } + } + this.friction = friction; + return this; + } + + @Override + public Builder lightEmission(Integer lightEmission) { + if (lightEmission != null) { + if (lightEmission < 0 || lightEmission > 15) { + throw new IllegalArgumentException("Light emission must be in the range 0-15"); + } + } + this.lightEmission = lightEmission; + return this; + } + + @Override + public Builder lightDampening(Integer lightDampening) { + if (lightDampening != null) { + if (lightDampening < 0 || lightDampening > 15) { + throw new IllegalArgumentException("Light dampening must be in the range 0-15"); + } + } + this.lightDampening = lightDampening; + return this; + } + + @Override + public Builder transformation(TransformationComponent transformation) { + if (transformation.rx() % 90 != 0 || transformation.ry() % 90 != 0 || transformation.rz() % 90 != 0) { + throw new IllegalArgumentException("Rotation of transformation must be a multiple of 90 degrees."); + } + this.transformation = transformation; + return this; + } + + @Override + public Builder unitCube(boolean unitCube) { + this.unitCube = unitCube; + return this; + } + + @Override + public Builder placeAir(boolean placeAir) { + this.placeAir = placeAir; + return this; + } + + @Override + public Builder tags(Set tags) { + this.tags.addAll(tags); + return this; + } + + @Override + public CustomBlockComponents build() { + return new GeyserCustomBlockComponents(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java new file mode 100644 index 000000000..413a4d1ed --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.*; +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; +import org.geysermc.geyser.api.util.CreativeCategory; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +public class GeyserCustomBlockData implements CustomBlockData { + private final String name; + private final boolean includedInCreativeInventory; + private final CreativeCategory creativeCategory; + private final String creativeGroup; + private final CustomBlockComponents components; + private final Map> properties; + private final List permutations; + + private final Map defaultProperties; + + GeyserCustomBlockData(CustomBlockDataBuilder builder) { + this.name = builder.name; + if (name == null) { + throw new IllegalStateException("Name must be set"); + } + + this.includedInCreativeInventory = builder.includedInCreativeInventory; + this.creativeCategory = builder.creativeCategory; + this.creativeGroup = builder.creativeGroup; + + this.components = builder.components; + + if (!builder.properties.isEmpty()) { + this.properties = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.properties)); + Object2ObjectMap defaultProperties = new Object2ObjectOpenHashMap<>(this.properties.size()); + for (CustomBlockProperty property : properties.values()) { + if (property.values().size() > 16) { + GeyserImpl.getInstance().getLogger().warning(property.name() + " contains more than 16 values, but BDS specifies it should not. This may break in future versions."); + } + if (property.values().stream().distinct().count() != property.values().size()) { + throw new IllegalStateException(property.name() + " has duplicate values."); + } + if (property.values().isEmpty()) { + throw new IllegalStateException(property.name() + " contains no values."); + } + defaultProperties.put(property.name(), property.values().get(0)); + } + this.defaultProperties = Object2ObjectMaps.unmodifiable(defaultProperties); + } else { + this.properties = Object2ObjectMaps.emptyMap(); + this.defaultProperties = Object2ObjectMaps.emptyMap(); + } + + if (!builder.permutations.isEmpty()) { + this.permutations = List.of(builder.permutations.toArray(new CustomBlockPermutation[0])); + } else { + this.permutations = ObjectLists.emptyList(); + } + } + + @Override + public @NonNull String name() { + return name; + } + + @Override + public @NonNull String identifier() { + return Constants.GEYSER_CUSTOM_NAMESPACE + ":" + name; + } + + @Override + public boolean includedInCreativeInventory() { + return includedInCreativeInventory; + } + + @Override + public @Nullable CreativeCategory creativeCategory() { + return creativeCategory; + } + + @Override + public @Nullable String creativeGroup() { + return creativeGroup; + } + + @Override + public CustomBlockComponents components() { + return components; + } + + @Override + public @NonNull Map> properties() { + return properties; + } + + @Override + public @NonNull List permutations() { + return permutations; + } + + @Override + public @NonNull CustomBlockState defaultBlockState() { + return new GeyserCustomBlockState(this, defaultProperties); + } + + @Override + public CustomBlockState.@NotNull Builder blockStateBuilder() { + return new GeyserCustomBlockState.CustomBlockStateBuilder(this); + } + + public static class CustomBlockDataBuilder implements Builder { + private String name; + private boolean includedInCreativeInventory; + private CreativeCategory creativeCategory; + private String creativeGroup; + private CustomBlockComponents components; + private final Object2ObjectMap> properties = new Object2ObjectOpenHashMap<>(); + private List permutations = ObjectLists.emptyList(); + + @Override + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + @Override + public Builder includedInCreativeInventory(boolean includedInCreativeInventory) { + this.includedInCreativeInventory = includedInCreativeInventory; + return this; + } + + @Override + public Builder creativeCategory(@Nullable CreativeCategory creativeCategory) { + this.creativeCategory = creativeCategory; + return this; + } + + @Override + public Builder creativeGroup(@Nullable String creativeGroup) { + this.creativeGroup = creativeGroup; + return this; + } + + @Override + public Builder components(@NonNull CustomBlockComponents components) { + this.components = components; + return this; + } + + @Override + public Builder booleanProperty(@NonNull String propertyName) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, List.of((byte) 0, (byte) 1), PropertyType.booleanProp())); + return this; + } + + @Override + public Builder intProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.integerProp())); + return this; + } + + @Override + public Builder stringProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.stringProp())); + return this; + } + + @Override + public Builder permutations(@NonNull List permutations) { + this.permutations = permutations; + return this; + } + + @Override + public CustomBlockData build() { + return new GeyserCustomBlockData(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java new file mode 100644 index 000000000..139df1fcd --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; + +import java.util.List; + +/** + * A custom block property that can be used to store custom data for a block. + * + * @param The type of the property + * @param name The name of the property + * @param values The values of the property + * @param type The type of the property + */ +public record GeyserCustomBlockProperty(@NonNull String name, @NonNull List values, + @NonNull PropertyType type) implements CustomBlockProperty { +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java new file mode 100644 index 000000000..576bd9743 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; + +import java.util.Map; + +@Value +public class GeyserCustomBlockState implements CustomBlockState { + CustomBlockData block; + Map properties; + + @Override + public @NonNull CustomBlockData block() { + return block; + } + + @Override + public @NonNull String name() { + return block.name(); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull T property(String propertyName) { + return (T) properties.get(propertyName); + } + + @Override + public @NonNull Map properties() { + return properties; + } + + @RequiredArgsConstructor + public static class CustomBlockStateBuilder implements CustomBlockState.Builder { + private final CustomBlockData blockData; + private final Object2ObjectMap properties = new Object2ObjectOpenHashMap<>(); + + @Override + public Builder booleanProperty(@NonNull String propertyName, boolean value) { + properties.put(propertyName, value ? (byte) 1 : (byte) 0); + return this; + } + + @Override + public Builder intProperty(@NonNull String propertyName, int value) { + properties.put(propertyName, value); + return this; + } + + @Override + public Builder stringProperty(@NonNull String propertyName, @NonNull String value) { + properties.put(propertyName, value); + return this; + } + + @Override + public CustomBlockState build() { + for (String propertyName : blockData.properties().keySet()) { + if (!properties.containsKey(propertyName)) { + throw new IllegalArgumentException("Missing property: " + propertyName); + } + } + + for (Map.Entry entry : properties.entrySet()) { + String propertyName = entry.getKey(); + Object propertyValue = entry.getValue(); + + CustomBlockProperty property = blockData.properties().get(propertyName); + if (property == null) { + throw new IllegalArgumentException("Unknown property: " + propertyName); + } else if (!property.values().contains(propertyValue)) { + throw new IllegalArgumentException("Invalid value: " + propertyValue + " for property: " + propertyName); + } + } + + return new GeyserCustomBlockState(blockData, Object2ObjectMaps.unmodifiable(properties)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java new file mode 100644 index 000000000..c23fc87a2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; + +import java.util.Map; + +@RequiredArgsConstructor +public class GeyserGeometryComponent implements GeometryComponent { + private final String identifier; + private final Map boneVisibility; + + GeyserGeometryComponent(GeometryComponentBuilder builder) { + this.identifier = builder.identifier; + this.boneVisibility = builder.boneVisibility; + } + + @Override + public @NonNull String identifier() { + return identifier; + } + + @Override + public @Nullable Map boneVisibility() { + return boneVisibility; + } + + public static class GeometryComponentBuilder implements Builder { + private String identifier; + private Map boneVisibility; + + @Override + public GeometryComponent.Builder identifier(@NonNull String identifier) { + this.identifier = identifier; + return this; + } + + @Override + public GeometryComponent.Builder boneVisibility(@Nullable Map boneVisibility) { + this.boneVisibility = boneVisibility; + return this; + } + + @Override + public GeometryComponent build() { + return new GeyserGeometryComponent(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java new file mode 100644 index 000000000..725afe6df --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java @@ -0,0 +1,161 @@ +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBoundingBox; + +public class GeyserJavaBlockState implements JavaBlockState { + String identifier; + int javaId; + int stateGroupId; + float blockHardness; + boolean waterlogged; + JavaBoundingBox[] collision; + boolean canBreakWithHand; + String pickItem; + String pistonBehavior; + boolean hasBlockEntity; + + private GeyserJavaBlockState(JavaBlockStateBuilder builder) { + this.identifier = builder.identifier; + this.javaId = builder.javaId; + this.stateGroupId = builder.stateGroupId; + this.blockHardness = builder.blockHardness; + this.waterlogged = builder.waterlogged; + this.collision = builder.collision; + this.canBreakWithHand = builder.canBreakWithHand; + this.pickItem = builder.pickItem; + this.pistonBehavior = builder.pistonBehavior; + this.hasBlockEntity = builder.hasBlockEntity; + } + + @Override + public @NonNull String identifier() { + return identifier; + } + + @Override + public @NonNegative int javaId() { + return javaId; + } + + @Override + public @NonNegative int stateGroupId() { + return stateGroupId; + } + + @Override + public @NonNegative float blockHardness() { + return blockHardness; + } + + @Override + public @NonNull boolean waterlogged() { + return waterlogged; + } + + @Override + public @NonNull JavaBoundingBox[] collision() { + return collision; + } + + @Override + public @NonNull boolean canBreakWithHand() { + return canBreakWithHand; + } + + @Override + public @Nullable String pickItem() { + return pickItem; + } + + @Override + public @Nullable String pistonBehavior() { + return pistonBehavior; + } + + @Override + public @Nullable boolean hasBlockEntity() { + return hasBlockEntity; + } + + public static class JavaBlockStateBuilder implements Builder { + private String identifier; + private int javaId; + private int stateGroupId; + private float blockHardness; + private boolean waterlogged; + private JavaBoundingBox[] collision; + private boolean canBreakWithHand; + private String pickItem; + private String pistonBehavior; + private boolean hasBlockEntity; + + @Override + public Builder identifier(@NonNull String identifier) { + this.identifier = identifier; + return this; + } + + @Override + public Builder javaId(@NonNegative int javaId) { + this.javaId = javaId; + return this; + } + + @Override + public Builder stateGroupId(@NonNegative int stateGroupId) { + this.stateGroupId = stateGroupId; + return this; + } + + @Override + public Builder blockHardness(@NonNegative float blockHardness) { + this.blockHardness = blockHardness; + return this; + } + + @Override + public Builder waterlogged(@NonNull boolean waterlogged) { + this.waterlogged = waterlogged; + return this; + } + + @Override + public Builder collision(@NonNull JavaBoundingBox[] collision) { + this.collision = collision; + return this; + } + + @Override + public Builder canBreakWithHand(@NonNull boolean canBreakWithHand) { + this.canBreakWithHand = canBreakWithHand; + return this; + } + + @Override + public Builder pickItem(@Nullable String pickItem) { + this.pickItem = pickItem; + return this; + } + + @Override + public Builder pistonBehavior(@Nullable String pistonBehavior) { + this.pistonBehavior = pistonBehavior; + return this; + } + + @Override + public Builder hasBlockEntity(@Nullable boolean hasBlockEntity) { + this.hasBlockEntity = hasBlockEntity; + return this; + } + + @Override + public JavaBlockState build() { + return new GeyserJavaBlockState(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java new file mode 100644 index 000000000..80a880152 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; + +@RequiredArgsConstructor +public class GeyserMaterialInstance implements MaterialInstance { + private final String texture; + private final String renderMethod; + private final boolean faceDimming; + private final boolean ambientOcclusion; + + GeyserMaterialInstance(MaterialInstanceBuilder builder) { + this.texture = builder.texture; + this.renderMethod = builder.renderMethod; + this.faceDimming = builder.faceDimming; + this.ambientOcclusion = builder.ambientOcclusion; + } + + @Override + public @NonNull String texture() { + return texture; + } + + @Override + public @Nullable String renderMethod() { + return renderMethod; + } + + @Override + public @Nullable boolean faceDimming() { + return faceDimming; + } + + @Override + public @Nullable boolean ambientOcclusion() { + return ambientOcclusion; + } + + public static class MaterialInstanceBuilder implements Builder { + private String texture; + private String renderMethod; + private boolean faceDimming; + private boolean ambientOcclusion; + + @Override + public Builder texture(@NonNull String texture) { + this.texture = texture; + return this; + } + + @Override + public Builder renderMethod(@Nullable String renderMethod) { + this.renderMethod = renderMethod; + return this; + } + + @Override + public Builder faceDimming(@Nullable boolean faceDimming) { + this.faceDimming = faceDimming; + return this; + } + + @Override + public Builder ambientOcclusion(@Nullable boolean ambientOcclusion) { + this.ambientOcclusion = ambientOcclusion; + return this; + } + + @Override + public MaterialInstance build() { + return new GeyserMaterialInstance(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java new file mode 100644 index 000000000..8b5056a6f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; + +public class GeyserNonVanillaCustomBlockData extends GeyserCustomBlockData implements NonVanillaCustomBlockData { + private final String namespace; + + GeyserNonVanillaCustomBlockData(NonVanillaCustomBlockDataBuilder builder) { + super(builder); + + this.namespace = builder.namespace; + if (namespace == null) { + throw new IllegalStateException("Identifier must be set"); + } + } + + @Override + public @NonNull String identifier() { + return this.namespace + ":" + super.name(); + } + + @Override + public @NonNull String namespace() { + return this.namespace; + } + + public static class NonVanillaCustomBlockDataBuilder extends CustomBlockDataBuilder implements NonVanillaCustomBlockData.Builder { + private String namespace; + + @Override + public NonVanillaCustomBlockDataBuilder namespace(@NonNull String namespace) { + this.namespace = namespace; + return this; + } + + @Override + public NonVanillaCustomBlockDataBuilder name(@NonNull String name) { + return (NonVanillaCustomBlockDataBuilder) super.name(name); + } + + @Override + public NonVanillaCustomBlockDataBuilder includedInCreativeInventory(boolean includedInCreativeInventory) { + return (NonVanillaCustomBlockDataBuilder) super.includedInCreativeInventory(includedInCreativeInventory); + } + + @Override + public NonVanillaCustomBlockDataBuilder creativeCategory(@Nullable CreativeCategory creativeCategories) { + return (NonVanillaCustomBlockDataBuilder) super.creativeCategory(creativeCategories); + } + + @Override + public NonVanillaCustomBlockDataBuilder creativeGroup(@Nullable String creativeGroup) { + return (NonVanillaCustomBlockDataBuilder) super.creativeGroup(creativeGroup); + } + + @Override + public NonVanillaCustomBlockDataBuilder components(@NonNull CustomBlockComponents components) { + return (NonVanillaCustomBlockDataBuilder) super.components(components); + } + + @Override + public NonVanillaCustomBlockDataBuilder booleanProperty(@NonNull String propertyName) { + return (NonVanillaCustomBlockDataBuilder) super.booleanProperty(propertyName); + } + + @Override + public NonVanillaCustomBlockDataBuilder intProperty(@NonNull String propertyName, List values) { + return (NonVanillaCustomBlockDataBuilder) super.intProperty(propertyName, values); + } + + @Override + public NonVanillaCustomBlockDataBuilder stringProperty(@NonNull String propertyName, List values) { + return (NonVanillaCustomBlockDataBuilder) super.stringProperty(propertyName, values); + } + + @Override + public NonVanillaCustomBlockDataBuilder permutations(@NonNull List permutations) { + return (NonVanillaCustomBlockDataBuilder) super.permutations(permutations); + } + + @Override + public NonVanillaCustomBlockData build() { + return new GeyserNonVanillaCustomBlockData(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java new file mode 100644 index 000000000..d9f7a6327 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.skin.SkinProvider; +import org.geysermc.geyser.util.FileUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.*; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class SkullResourcePackManager { + + private static final long RESOURCE_PACK_VERSION = 8; + + private static final Path SKULL_SKIN_CACHE_PATH = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("player_skulls"); + + public static final Map SKULL_SKINS = new Object2ObjectOpenHashMap<>(); + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static Path createResourcePack() { + Path cachePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache"); + try { + Files.createDirectories(cachePath); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to create directories for player skull resource pack!", e); + return null; + } + cleanSkullSkinCache(); + + Path packPath = cachePath.resolve("player_skulls.mcpack"); + File packFile = packPath.toFile(); + if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + packFile.delete(); // No need to keep resource pack + return null; + } + if (packFile.exists() && canReusePack(packFile)) { + GeyserImpl.getInstance().getLogger().info("Reusing cached player skull resource pack."); + return packPath; + } + + // We need to create the resource pack from scratch + GeyserImpl.getInstance().getLogger().info("Creating skull resource pack."); + packFile.delete(); + try (ZipOutputStream zipOS = new ZipOutputStream(Files.newOutputStream(packPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE))) { + addBaseResources(zipOS); + addSkinTextures(zipOS); + addAttachables(zipOS); + GeyserImpl.getInstance().getLogger().info("Finished creating skull resource pack."); + return packPath; + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to create player skull resource pack!", e); + GeyserImpl.getInstance().getLogger().severe("Bedrock players will see dirt blocks instead of custom skull blocks."); + packFile.delete(); + } + return null; + } + + public static void cacheSkullSkin(String skinHash) throws IOException { + String skinUrl = Constants.MINECRAFT_SKIN_SERVER_URL + skinHash; + Path skinPath = SKULL_SKINS.get(skinHash); + if (skinPath != null) { + return; + } + + Files.createDirectories(SKULL_SKIN_CACHE_PATH); + skinPath = SKULL_SKIN_CACHE_PATH.resolve(skinHash + ".png"); + if (Files.exists(skinPath)) { + SKULL_SKINS.put(skinHash, skinPath); + return; + } + + BufferedImage image = SkinProvider.requestImage(skinUrl, null); + // Resize skins to 48x16 to save on space and memory + BufferedImage skullTexture = new BufferedImage(48, 16, image.getType()); + // Reorder skin parts to fit into the space + // Right, Front, Left, Back, Top, Bottom - head + // Right, Front, Left, Back, Top, Bottom - hat + Graphics g = skullTexture.createGraphics(); + // Right, Front, Left, Back of the head + g.drawImage(image, 0, 0, 32, 8, 0, 8, 32, 16, null); + // Right, Front, Left, Back of the hat + g.drawImage(image, 0, 8, 32, 16, 32, 8, 64, 16, null); + // Top and bottom of the head + g.drawImage(image, 32, 0, 48, 8, 8, 0, 24, 8, null); + // Top and bottom of the hat + g.drawImage(image, 32, 8, 48, 16, 40, 0, 56, 8, null); + g.dispose(); + image.flush(); + + ImageIO.write(skullTexture, "png", skinPath.toFile()); + SKULL_SKINS.put(skinHash, skinPath); + GeyserImpl.getInstance().getLogger().debug("Cached player skull to " + skinPath + " for " + skinHash); + } + + public static void cleanSkullSkinCache() { + try (Stream stream = Files.list(SKULL_SKIN_CACHE_PATH)) { + int removeCount = 0; + for (Path path : stream.toList()) { + String skinHash = path.getFileName().toString(); + skinHash = skinHash.substring(0, skinHash.length() - ".png".length()); + if (!SKULL_SKINS.containsKey(skinHash) && path.toFile().delete()) { + removeCount++; + } + } + if (removeCount != 0) { + GeyserImpl.getInstance().getLogger().debug("Removed " + removeCount + " unnecessary skull skins."); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().debug("Unable to clean up skull skin cache."); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + } + + private static void addBaseResources(ZipOutputStream zipOS) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(GeyserImpl.getInstance().getBootstrap().getResource("bedrock/skull_resource_pack_files.txt")))) { + List lines = reader.lines().toList(); + for (String path : lines) { + ZipEntry entry = new ZipEntry(path); + + zipOS.putNextEntry(entry); + String resourcePath = "bedrock/" + path; + switch (path) { + case "skull_resource_pack/manifest.json" -> + fillTemplate(zipOS, resourcePath, SkullResourcePackManager::fillManifestJson); + case "skull_resource_pack/textures/terrain_texture.json" -> + fillTemplate(zipOS, resourcePath, SkullResourcePackManager::fillTerrainTextureJson); + default -> zipOS.write(FileUtils.readAllBytes(resourcePath)); + } + zipOS.closeEntry(); + } + + addFloorGeometries(zipOS); + + ZipEntry entry = new ZipEntry("skull_resource_pack/pack_icon.png"); + zipOS.putNextEntry(entry); + zipOS.write(FileUtils.readAllBytes("icon.png")); + zipOS.closeEntry(); + } + } + + private static void addFloorGeometries(ZipOutputStream zipOS) throws IOException { + String template = FileUtils.readToString("bedrock/skull_resource_pack/models/blocks/player_skull_floor.geo.json"); + String[] quadrants = {"a", "b", "c", "d"}; + for (int i = 0; i < quadrants.length; i++) { + String quadrant = quadrants[i]; + float yRotation = i * 22.5f; + String contents = template + .replace("${quadrant}", quadrant) + .replace("${y_rotation}", String.valueOf(yRotation)); + + ZipEntry entry = new ZipEntry("skull_resource_pack/models/blocks/player_skull_floor_" + quadrant + ".geo.json"); + zipOS.putNextEntry(entry); + zipOS.write(contents.getBytes(StandardCharsets.UTF_8)); + zipOS.closeEntry(); + } + } + + private static void addAttachables(ZipOutputStream zipOS) throws IOException { + String template = FileUtils.readToString("bedrock/skull_resource_pack/attachables/template_attachable.json"); + for (CustomSkull skull : BlockRegistries.CUSTOM_SKULLS.get().values()) { + ZipEntry entry = new ZipEntry("skull_resource_pack/attachables/" + truncateHash(skull.getSkinHash()) + ".json"); + zipOS.putNextEntry(entry); + zipOS.write(fillAttachableJson(template, skull).getBytes(StandardCharsets.UTF_8)); + zipOS.closeEntry(); + } + } + + private static void addSkinTextures(ZipOutputStream zipOS) throws IOException { + for (Path skinPath : SKULL_SKINS.values()) { + ZipEntry entry = new ZipEntry("skull_resource_pack/textures/blocks/" + truncateHash(skinPath.getFileName().toString()) + ".png"); + zipOS.putNextEntry(entry); + try (InputStream stream = Files.newInputStream(skinPath)) { + stream.transferTo(zipOS); + } + zipOS.closeEntry(); + } + } + + private static void fillTemplate(ZipOutputStream zipOS, String path, UnaryOperator filler) throws IOException { + String template = FileUtils.readToString(path); + String result = filler.apply(template); + zipOS.write(result.getBytes(StandardCharsets.UTF_8)); + } + + private static String fillAttachableJson(String template, CustomSkull skull) { + return template.replace("${identifier}", skull.getCustomBlockData().identifier()) + .replace("${texture}", truncateHash(skull.getSkinHash())); + } + + private static String fillManifestJson(String template) { + Pair uuids = generatePackUUIDs(); + return template.replace("${uuid1}", uuids.first().toString()) + .replace("${uuid2}", uuids.second().toString()); + } + + private static String fillTerrainTextureJson(String template) { + StringBuilder textures = new StringBuilder(); + for (String skinHash : SKULL_SKINS.keySet()) { + String texture = String.format("\"geyser.%s_player_skin\":{\"textures\":\"textures/blocks/%s\"},\n", skinHash, truncateHash(skinHash)); + textures.append(texture); + } + if (textures.length() != 0) { + // Remove trailing comma + textures.delete(textures.length() - 2, textures.length()); + } + return template.replace("${texture_data}", textures); + } + + private static Pair generatePackUUIDs() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + for (int i = 0; i < 8; i++) { + md.update((byte) ((RESOURCE_PACK_VERSION >> (i * 8)) & 0xFF)); + } + SKULL_SKINS.keySet().stream() + .sorted() + .map(hash -> hash.getBytes(StandardCharsets.UTF_8)) + .forEach(md::update); + + ByteBuffer skinHashes = ByteBuffer.wrap(md.digest()); + uuid1 = new UUID(skinHashes.getLong(), skinHashes.getLong()); + uuid2 = new UUID(skinHashes.getLong(), skinHashes.getLong()); + } catch (NoSuchAlgorithmException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to get SHA-256 Message Digest instance! Bedrock players will have to re-downloaded the player skull resource pack after each server restart.", e); + } + + return Pair.of(uuid1, uuid2); + } + + private static boolean canReusePack(File packFile) { + Pair uuids = generatePackUUIDs(); + try (ZipFile zipFile = new ZipFile(packFile)) { + Optional manifestEntry = zipFile.stream() + .filter(entry -> entry.getName().contains("manifest.json")) + .findFirst(); + if (manifestEntry.isPresent()) { + GeyserResourcePackManifest manifest = FileUtils.loadJson(zipFile.getInputStream(manifestEntry.get()), GeyserResourcePackManifest.class); + if (!uuids.first().equals(manifest.header().uuid())) { + return false; + } + Optional resourceUUID = manifest.modules().stream() + .filter(module -> "resources".equals(module.type())) + .findFirst() + .map(ResourcePackManifest.Module::uuid); + return resourceUUID.isPresent() && uuids.second().equals(resourceUUID.get()); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().debug("Cached player skull resource pack was invalid! The pack will be recreated."); + } + return false; + } + + private static String truncateHash(String hash) { + return hash.substring(0, Math.min(hash.length(), 32)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java index 8b576a673..49fd90cf7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java @@ -25,17 +25,30 @@ package org.geysermc.geyser.registry; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.registry.loader.CollisionRegistryLoader; import org.geysermc.geyser.registry.loader.RegistryLoaders; import org.geysermc.geyser.registry.populator.BlockRegistryPopulator; +import org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator; +import org.geysermc.geyser.registry.populator.CustomSkullRegistryPopulator; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.BlockMappings; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.translator.collision.BlockCollision; import java.util.BitSet; +import java.util.Set; + /** * Holds all the block registries in Geyser. */ @@ -57,6 +70,11 @@ public class BlockRegistries { */ public static final ArrayRegistry JAVA_BLOCKS = ArrayRegistry.create(RegistryLoaders.uninitialized()); + /** + * A mapped registry containing which holds block IDs to its {@link BlockCollision}. + */ + public static final IntMappedRegistry COLLISIONS; + /** * A mapped registry containing the Java identifiers to IDs. */ @@ -83,11 +101,51 @@ public class BlockRegistries { */ public static final SimpleRegistry INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.uninitialized()); + /** + * A registry containing all the custom blocks. + */ + public static final ArrayRegistry CUSTOM_BLOCKS = ArrayRegistry.create(RegistryLoaders.empty(() -> new CustomBlockData[] {})); + + /** + * A registry which stores Java Ids and the custom block state it should be replaced with. + */ + public static final MappedRegistry> CUSTOM_BLOCK_STATE_OVERRIDES = MappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + + /** + * A registry which stores non vanilla java blockstates and the custom block state it should be replaced with. + */ + public static final SimpleMappedRegistry NON_VANILLA_BLOCK_STATE_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores clean Java Ids and the custom block it should be replaced with in the context of items. + */ + public static final SimpleMappedRegistry CUSTOM_BLOCK_ITEM_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores Custom Block Data for extended collision boxes and the Java IDs of blocks that will have said extended collision boxes placed above them. + */ + public static final SimpleMappedRegistry> EXTENDED_COLLISION_BOXES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores skin texture hashes to custom skull blocks. + */ + public static final SimpleMappedRegistry CUSTOM_SKULLS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + static { - BlockRegistryPopulator.populate(); + CustomSkullRegistryPopulator.populate(); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.PRE_INIT); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.DEFINITION); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.NON_VANILLA_REGISTRATION); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_JAVA); + COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.VANILLA_REGISTRATION); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.CUSTOM_REGISTRATION); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_BEDROCK); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.POST_INIT); } public static void init() { // no-op } + } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 73ae23e9e..9b5ed8ae6 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -31,7 +31,6 @@ import com.github.steveice10.mc.protocol.data.game.level.event.LevelEvent; import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType; import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; import com.github.steveice10.packetlib.packet.Packet; -import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -56,7 +55,6 @@ import org.geysermc.geyser.registry.type.EnchantmentData; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ParticleMapping; import org.geysermc.geyser.registry.type.SoundMapping; -import org.geysermc.geyser.translator.collision.BlockCollision; import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator; import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.sound.SoundInteractionTranslator; @@ -68,6 +66,12 @@ import java.util.*; * Holds all the common registries in Geyser. */ public final class Registries { + /** + * A registry holding all the providers. + * This has to be initialized first to allow extensions to access providers during other registry events. + */ + public static final SimpleMappedRegistry, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new); + /** * A registry holding a CompoundTag of the known entity identifiers. */ @@ -93,11 +97,6 @@ public final class Registries { */ public static final SimpleMappedRegistry BLOCK_ENTITIES = SimpleMappedRegistry.create("org.geysermc.geyser.translator.level.block.entity.BlockEntity", BlockEntityRegistryLoader::new); - /** - * A mapped registry containing which holds block IDs to its {@link BlockCollision}. - */ - public static final IntMappedRegistry COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new); - /** * A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link RecipeData}. */ @@ -144,11 +143,6 @@ public final class Registries { */ public static final VersionedRegistry> POTION_MIXES; - /** - * A registry holding all the - */ - public static final SimpleMappedRegistry, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new); - /** * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value. */ diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java index 69ad16743..bf2d72b27 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java @@ -79,6 +79,11 @@ public class CollisionRegistryLoader extends MultiResourceRegistryLoader collisionInstances = new Object2ObjectOpenHashMap<>(); for (int i = 0; i < blockMappings.length; i++) { BlockMapping blockMapping = blockMappings[i]; + if (blockMapping == null) { + GeyserImpl.getInstance().getLogger().warning("Missing block mapping for Java block " + i); + continue; + } + BlockCollision newCollision = instantiateCollision(blockMapping, annotationMap, collisionList); if (newCollision != null) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index d32b11cc0..13d7a4d77 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -25,6 +25,12 @@ package org.geysermc.geyser.registry.loader; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.extension.Extension; @@ -37,6 +43,12 @@ import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; +import org.geysermc.geyser.level.block.GeyserCustomBlockData; +import org.geysermc.geyser.level.block.GeyserGeometryComponent; +import org.geysermc.geyser.level.block.GeyserJavaBlockState; +import org.geysermc.geyser.level.block.GeyserMaterialInstance; +import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; @@ -52,6 +64,14 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { // misc providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Extension) args[0])); + + providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()); + providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.CustomBlockDataBuilder()); + providers.put(JavaBlockState.Builder.class, args -> new GeyserJavaBlockState.JavaBlockStateBuilder()); + providers.put(NonVanillaCustomBlockData.Builder.class, args -> new GeyserNonVanillaCustomBlockData.NonVanillaCustomBlockDataBuilder()); + providers.put(MaterialInstance.Builder.class, args -> new GeyserMaterialInstance.MaterialInstanceBuilder()); + providers.put(GeometryComponent.Builder.class, args -> new GeyserGeometryComponent.GeometryComponentBuilder()); + providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java index 452550d87..800a3d22c 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -30,6 +30,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; +import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; @@ -87,6 +88,12 @@ public class ResourcePackLoader implements RegistryLoader(); } + // Add custom skull pack + Path skullResourcePack = SkullResourcePackManager.createResourcePack(); + if (skullResourcePack != null) { + resourcePacks.add(skullResourcePack); + } + GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java similarity index 58% rename from core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java rename to core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java index eaf07c382..039412957 100644 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java @@ -23,15 +23,16 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.mappings; +package org.geysermc.geyser.registry.mappings; import com.fasterxml.jackson.databind.JsonNode; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.item.custom.CustomItemData; -import org.geysermc.geyser.item.mappings.versions.MappingsReader; -import org.geysermc.geyser.item.mappings.versions.MappingsReader_v1; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; +import org.geysermc.geyser.registry.mappings.versions.MappingsReader; +import org.geysermc.geyser.registry.mappings.versions.MappingsReader_v1; import java.io.IOException; import java.nio.file.Files; @@ -56,43 +57,88 @@ public class MappingsConfigReader { } } - public void loadMappingsFromJson(BiConsumer consumer) { - Path customMappingsDirectory = this.customMappingsDirectory; - if (!Files.exists(customMappingsDirectory)) { + public boolean ensureMappingsDirectory(Path mappingsDirectory) { + if (!Files.exists(mappingsDirectory)) { try { - Files.createDirectories(customMappingsDirectory); + Files.createDirectories(mappingsDirectory); + return true; } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to create custom mappings directory", e); - return; + GeyserImpl.getInstance().getLogger().error("Failed to create mappings directory", e); + return false; } } + return true; + } + + public void loadItemMappingsFromJson(BiConsumer consumer) { + if (!ensureMappingsDirectory(this.customMappingsDirectory)) { + return; + } Path[] mappingsFiles = this.getCustomMappingsFiles(); for (Path mappingsFile : mappingsFiles) { - this.readMappingsFromJson(mappingsFile, consumer); + this.readItemMappingsFromJson(mappingsFile, consumer); } } - public void readMappingsFromJson(Path file, BiConsumer consumer) { + public void loadBlockMappingsFromJson(BiConsumer consumer) { + if (!ensureMappingsDirectory(this.customMappingsDirectory)) { + return; + } + + Path[] mappingsFiles = this.getCustomMappingsFiles(); + for (Path mappingsFile : mappingsFiles) { + this.readBlockMappingsFromJson(mappingsFile, consumer); + } + } + + public JsonNode getMappingsRoot(Path file) { JsonNode mappingsRoot; try { mappingsRoot = GeyserImpl.JSON_MAPPER.readTree(file.toFile()); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Failed to read custom mapping file: " + file, e); - return; + return null; } if (!mappingsRoot.has("format_version")) { GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " is missing the format version field!"); - return; + return null; } - int formatVersion = mappingsRoot.get("format_version").asInt(); + return mappingsRoot; + } + + public int getFormatVersion(JsonNode mappingsRoot, Path file) { + int formatVersion = mappingsRoot.get("format_version").asInt(); if (!this.mappingReaders.containsKey(formatVersion)) { GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " has an unknown format version: " + formatVersion); + return -1; + } + return formatVersion; + } + + public void readItemMappingsFromJson(Path file, BiConsumer consumer) { + JsonNode mappingsRoot = getMappingsRoot(file); + + int formatVersion = getFormatVersion(mappingsRoot, file); + + if (formatVersion < 0 || mappingsRoot == null) { return; } - this.mappingReaders.get(formatVersion).readMappings(file, mappingsRoot, consumer); + this.mappingReaders.get(formatVersion).readItemMappings(file, mappingsRoot, consumer); + } + + public void readBlockMappingsFromJson(Path file, BiConsumer consumer) { + JsonNode mappingsRoot = getMappingsRoot(file); + + int formatVersion = getFormatVersion(mappingsRoot, file); + + if (formatVersion < 0 || mappingsRoot == null) { + return; + } + + this.mappingReaders.get(formatVersion).readBlockMappings(file, mappingsRoot, consumer); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java new file mode 100644 index 000000000..3e5d934ab --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; + +/** + * This class is used to store a custom block components mapping, which contains custom + * block components and a potenially null extended collision box + * + * @param components The components of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockComponentsMapping(@NonNull CustomBlockComponents components, BoxComponent extendedCollisionBox) { +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java new file mode 100644 index 000000000..3dbb7908e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import java.util.Map; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; + +/** + * This class is used to store a custom block mappings, which contain all of the + * data required to register a custom block that overrides a group of java block + * states. + * + * @param data The custom block data + * @param states The custom block state mappings + * @param javaIdentifier The java identifier of the block + * @param overrideItem Whether or not the custom block should override the java item + */ +public record CustomBlockMapping(@NonNull CustomBlockData data, @NonNull Map states, @NonNull String javaIdentifier, boolean overrideItem) { +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java new file mode 100644 index 000000000..e627c04a6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; + +import java.util.function.Function; + +/** + * This class is used to store a custom block state builder mapping, which contains custom + * block state builders and a potenially null extended collision box + * + * @param builder The builder of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockStateBuilderMapping(@NonNull Function builder, BoxComponent extendedCollisionBox) { +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java new file mode 100644 index 000000000..4c04bd657 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; + +/** + * This class is used to store a custom block state mapping, which contains custom + * block states and a potenially null extended collision box + * + * @param state The state of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockStateMapping(@NonNull CustomBlockState state, BoxComponent extendedCollisionBox) { +} diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java similarity index 86% rename from core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java rename to core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java index ef553f488..e76df2834 100644 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java @@ -23,20 +23,23 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.mappings.versions; +package org.geysermc.geyser.registry.mappings.versions; import com.fasterxml.jackson.databind.JsonNode; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; import java.nio.file.Path; import java.util.function.BiConsumer; public abstract class MappingsReader { - public abstract void readMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); + public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); + public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); public abstract CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException; + public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException; protected CustomRenderOffsets fromJsonNode(JsonNode node) { if (node == null || !node.isObject()) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java new file mode 100644 index 000000000..51c2220cb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java @@ -0,0 +1,713 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.versions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.github.steveice10.mc.protocol.data.game.Identifier; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.*; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions.BlockFilterType; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face; +import org.geysermc.geyser.api.item.custom.CustomItemData; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.util.CreativeCategory; +import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder; +import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.mappings.util.CustomBlockComponentsMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockStateBuilderMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockStateMapping; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.MathUtils; + +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * A class responsible for reading custom item and block mappings from a JSON file + */ +public class MappingsReader_v1 extends MappingsReader { + @Override + public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + this.readItemMappingsV1(file, mappingsRoot, consumer); + } + + /** + * Read item block from a JSON node + * + * @param file The path to the file + * @param mappingsRoot The {@link JsonNode} containing the mappings + * @param consumer The consumer to accept the mappings + * @see #readBlockMappingsV1(Path, JsonNode, BiConsumer) + */ + @Override + public void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + this.readBlockMappingsV1(file, mappingsRoot, consumer); + } + + public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + JsonNode itemsNode = mappingsRoot.get("items"); + + if (itemsNode != null && itemsNode.isObject()) { + itemsNode.fields().forEachRemaining(entry -> { + if (entry.getValue().isArray()) { + entry.getValue().forEach(data -> { + try { + CustomItemData customItemData = this.readItemMappingEntry(data); + consumer.accept(entry.getKey(), customItemData); + } catch (InvalidCustomMappingsFileException e) { + GeyserImpl.getInstance().getLogger().error("Error in registering items for custom mapping file: " + file.toString(), e); + } + }); + } + }); + } + } + + /** + * Read block mappings from a JSON node + * + * @param file The path to the file + * @param mappingsRoot The {@link JsonNode} containing the mappings + * @param consumer The consumer to accept the mappings + * @see #readBlockMappings(Path, JsonNode, BiConsumer) + */ + public void readBlockMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + JsonNode blocksNode = mappingsRoot.get("blocks"); + + if (blocksNode != null && blocksNode.isObject()) { + blocksNode.fields().forEachRemaining(entry -> { + if (entry.getValue().isObject()) { + try { + String identifier = Identifier.formalize(entry.getKey()); + CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue()); + consumer.accept(identifier, customBlockMapping); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Error in registering blocks for custom mapping file: " + file.toString()); + GeyserImpl.getInstance().getLogger().error("due to entry: " + entry, e); + } + } + }); + } + } + + private CustomItemOptions readItemCustomItemOptions(JsonNode node) { + CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder(); + + JsonNode customModelData = node.get("custom_model_data"); + if (customModelData != null && customModelData.isInt()) { + customItemOptions.customModelData(customModelData.asInt()); + } + + JsonNode damagePredicate = node.get("damage_predicate"); + if (damagePredicate != null && damagePredicate.isInt()) { + customItemOptions.damagePredicate(damagePredicate.asInt()); + } + + JsonNode unbreakable = node.get("unbreakable"); + if (unbreakable != null && unbreakable.isBoolean()) { + customItemOptions.unbreakable(unbreakable.asBoolean()); + } + + JsonNode defaultItem = node.get("default"); + if (defaultItem != null && defaultItem.isBoolean()) { + customItemOptions.defaultItem(defaultItem.asBoolean()); + } + + return customItemOptions.build(); + } + + @Override + public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException { + if (node == null || !node.isObject()) { + throw new InvalidCustomMappingsFileException("Invalid item mappings entry"); + } + + JsonNode name = node.get("name"); + if (name == null || !name.isTextual() || name.asText().isEmpty()) { + throw new InvalidCustomMappingsFileException("An item entry has no name"); + } + + CustomItemData.Builder customItemData = CustomItemData.builder() + .name(name.asText()) + .customItemOptions(this.readItemCustomItemOptions(node)); + + //The next entries are optional + if (node.has("display_name")) { + customItemData.displayName(node.get("display_name").asText()); + } + + if (node.has("icon")) { + customItemData.icon(node.get("icon").asText()); + } + + if (node.has("allow_offhand")) { + customItemData.allowOffhand(node.get("allow_offhand").asBoolean()); + } + + if (node.has("display_handheld")) { + customItemData.displayHandheld(node.get("display_handheld").asBoolean()); + } + + if (node.has("texture_size")) { + customItemData.textureSize(node.get("texture_size").asInt()); + } + + if (node.has("render_offsets")) { + JsonNode tmpNode = node.get("render_offsets"); + + customItemData.renderOffsets(fromJsonNode(tmpNode)); + } + + return customItemData.build(); + } + + /** + * Read a block mapping entry from a JSON node and Java identifier + * + * @param identifier The Java identifier of the block + * @param node The {@link JsonNode} containing the block mapping entry + * @return The {@link CustomBlockMapping} record to be read by {@link org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator} + * @throws InvalidCustomMappingsFileException If the JSON node is invalid + */ + @Override + public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException { + if (node == null || !node.isObject()) { + throw new InvalidCustomMappingsFileException("Invalid block mappings entry:" + node); + } + + String name = node.get("name").asText(); + if (name == null || name.isEmpty()) { + throw new InvalidCustomMappingsFileException("A block entry has no name"); + } + + boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").asBoolean(); + + CreativeCategory creativeCategory = CreativeCategory.NONE; + if (node.has("creative_category")) { + String categoryName = node.get("creative_category").asText(); + try { + creativeCategory = CreativeCategory.valueOf(categoryName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new InvalidCustomMappingsFileException("Invalid creative category \"" + categoryName + "\" for block \"" + name + "\""); + } + } + + String creativeGroup = ""; + if (node.has("creative_group")) { + creativeGroup = node.get("creative_group").asText(); + } + + // If this is true, we will only register the states the user has specified rather than all the possible block states + boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean(); + + // Create the data for the overall block + CustomBlockData.Builder customBlockDataBuilder = new CustomBlockDataBuilder() + .name(name) + .includedInCreativeInventory(includedInCreativeInventory) + .creativeCategory(creativeCategory) + .creativeGroup(creativeGroup); + + if (BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(identifier)) { + // There is only one Java block state to override + CustomBlockComponentsMapping componentsMapping = createCustomBlockComponentsMapping(node, identifier, name); + CustomBlockData blockData = customBlockDataBuilder + .components(componentsMapping.components()) + .build(); + return new CustomBlockMapping(blockData, Map.of(identifier, new CustomBlockStateMapping(blockData.defaultBlockState(), componentsMapping.extendedCollisionBox())), identifier, !onlyOverrideStates); + } + + Map componentsMap = new LinkedHashMap<>(); + + JsonNode stateOverrides = node.get("state_overrides"); + if (stateOverrides != null && stateOverrides.isObject()) { + // Load components for specific Java block states + Iterator> fields = stateOverrides.fields(); + while (fields.hasNext()) { + Map.Entry overrideEntry = fields.next(); + String state = identifier + "[" + overrideEntry.getKey() + "]"; + if (!BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(state)) { + throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides."); + } + componentsMap.put(state, createCustomBlockComponentsMapping(overrideEntry.getValue(), state, name)); + } + } + if (componentsMap.isEmpty() && onlyOverrideStates) { + throw new InvalidCustomMappingsFileException("Block entry for " + identifier + " has only_override_states set to true, but has no state_overrides."); + } + + if (!onlyOverrideStates) { + // Create components for any remaining Java block states + BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().keySet() + .stream() + .filter(s -> s.startsWith(identifier + "[")) + .filter(Predicate.not(componentsMap::containsKey)) + .forEach(state -> componentsMap.put(state, createCustomBlockComponentsMapping(null, state, name))); + } + + if (componentsMap.isEmpty()) { + throw new InvalidCustomMappingsFileException("Unknown Java block: " + identifier); + } + + // We pass in the first state and just use the hitbox from that as the default + // Each state will have its own so this is fine + String firstState = componentsMap.keySet().iterator().next(); + CustomBlockComponentsMapping firstComponentsMapping = createCustomBlockComponentsMapping(node, firstState, name); + customBlockDataBuilder.components(firstComponentsMapping.components()); + + return createCustomBlockMapping(customBlockDataBuilder, componentsMap, identifier, !onlyOverrideStates); + } + + private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder customBlockDataBuilder, Map componentsMap, String identifier, boolean overrideItem) { + Map> valuesMap = new Object2ObjectOpenHashMap<>(); + + List permutations = new ArrayList<>(); + Map blockStateBuilders = new Object2ObjectOpenHashMap<>(); + + // For each Java block state, extract the property values, create a CustomBlockPermutation, + // and a CustomBlockState builder + for (Map.Entry entry : componentsMap.entrySet()) { + String state = entry.getKey(); + String[] pairs = splitStateString(state); + + String[] conditions = new String[pairs.length]; + Function blockStateBuilder = Function.identity(); + + for (int i = 0; i < pairs.length; i++) { + String[] parts = pairs[i].split("="); + String property = parts[0]; + String value = parts[1]; + + valuesMap.computeIfAbsent(property, k -> new LinkedHashSet<>()) + .add(value); + + conditions[i] = String.format("q.block_property('%s') == '%s'", property, value); + blockStateBuilder = blockStateBuilder.andThen(builder -> builder.stringProperty(property, value)); + } + + permutations.add(new CustomBlockPermutation(entry.getValue().components(), String.join(" && ", conditions))); + blockStateBuilders.put(state, new CustomBlockStateBuilderMapping(blockStateBuilder.andThen(CustomBlockState.Builder::build), entry.getValue().extendedCollisionBox())); + } + + valuesMap.forEach((key, value) -> customBlockDataBuilder.stringProperty(key, new ArrayList<>(value))); + + CustomBlockData customBlockData = customBlockDataBuilder + .permutations(permutations) + .build(); + // Build CustomBlockStates for each Java block state we wish to override + Map states = blockStateBuilders.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> new CustomBlockStateMapping(e.getValue().builder().apply(customBlockData.blockStateBuilder()), e.getValue().extendedCollisionBox()))); + + return new CustomBlockMapping(customBlockData, states, identifier, overrideItem); + } + + /** + * Creates a {@link CustomBlockComponents} object for the passed state override or base block node, Java block state identifier, and custom block name + * + * @param node the state override or base block {@link JsonNode} + * @param stateKey the Java block state identifier + * @param name the name of the custom block + * @return the {@link CustomBlockComponents} object + */ + private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode node, String stateKey, String name) { + // This is needed to find the correct selection box for the given block + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(stateKey, -1); + BoxComponent boxComponent = createBoxComponent(id); + BoxComponent extendedBoxComponent = createExtendedBoxComponent(id); + CustomBlockComponents.Builder builder = new CustomBlockComponentsBuilder() + .collisionBox(boxComponent) + .selectionBox(boxComponent); + + if (node == null) { + // No other components were defined + return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent); + } + + BoxComponent selectionBox = createBoxComponent(node.get("selection_box")); + if (selectionBox != null) { + builder.selectionBox(selectionBox); + } + BoxComponent collisionBox = createBoxComponent(node.get("collision_box")); + if (collisionBox != null) { + builder.collisionBox(collisionBox); + } + BoxComponent extendedCollisionBox = createBoxComponent(node.get("extended_collision_box")); + if (extendedCollisionBox != null) { + extendedBoxComponent = extendedCollisionBox; + } + + + // We set this to max value by default so that we may dictate the correct destroy time ourselves + float destructibleByMining = Float.MAX_VALUE; + if (node.has("destructible_by_mining")) { + destructibleByMining = node.get("destructible_by_mining").floatValue(); + } + builder.destructibleByMining(destructibleByMining); + + if (node.has("geometry")) { + if (node.get("geometry").isTextual()) { + builder.geometry(new GeometryComponentBuilder() + .identifier(node.get("geometry").asText()) + .build()); + } else { + JsonNode geometry = node.get("geometry"); + GeometryComponentBuilder geometryBuilder = new GeometryComponentBuilder(); + if (geometry.has("identifier")) { + geometryBuilder.identifier(geometry.get("identifier").asText()); + } + if (geometry.has("bone_visibility")) { + JsonNode boneVisibility = geometry.get("bone_visibility"); + if (boneVisibility.isObject()) { + Map boneVisibilityMap = new Object2ObjectOpenHashMap<>(); + boneVisibility.fields().forEachRemaining(entry -> { + boneVisibilityMap.put(entry.getKey(), entry.getValue().isBoolean() ? (entry.getValue().asBoolean() ? "1" : "0") : entry.getValue().asText()); + }); + geometryBuilder.boneVisibility(boneVisibilityMap); + } + } + builder.geometry(geometryBuilder.build()); + } + } + + String displayName = name; + if (node.has("display_name")) { + displayName = node.get("display_name").asText(); + } + builder.displayName(displayName); + + if (node.has("friction")) { + builder.friction(node.get("friction").floatValue()); + } + + if (node.has("light_emission")) { + builder.lightEmission(node.get("light_emission").asInt()); + } + + if (node.has("light_dampening")) { + builder.lightDampening(node.get("light_dampening").asInt()); + } + + boolean placeAir = true; + if (node.has("place_air")) { + placeAir = node.get("place_air").asBoolean(); + } + builder.placeAir(placeAir); + + if (node.has("transformation")) { + JsonNode transformation = node.get("transformation"); + + int rotationX = 0; + int rotationY = 0; + int rotationZ = 0; + float scaleX = 1; + float scaleY = 1; + float scaleZ = 1; + float transformX = 0; + float transformY = 0; + float transformZ = 0; + + if (transformation.has("rotation")) { + JsonNode rotation = transformation.get("rotation"); + rotationX = rotation.get(0).asInt(); + rotationY = rotation.get(1).asInt(); + rotationZ = rotation.get(2).asInt(); + } + if (transformation.has("scale")) { + JsonNode scale = transformation.get("scale"); + scaleX = scale.get(0).floatValue(); + scaleY = scale.get(1).floatValue(); + scaleZ = scale.get(2).floatValue(); + } + if (transformation.has("translation")) { + JsonNode translation = transformation.get("translation"); + transformX = translation.get(0).floatValue(); + transformY = translation.get(1).floatValue(); + transformZ = translation.get(2).floatValue(); + } + builder.transformation(new TransformationComponent(rotationX, rotationY, rotationZ, scaleX, scaleY, scaleZ, transformX, transformY, transformZ)); + } + + if (node.has("unit_cube")) { + builder.unitCube(node.get("unit_cube").asBoolean()); + } + + if (node.has("material_instances")) { + JsonNode materialInstances = node.get("material_instances"); + if (materialInstances.isObject()) { + materialInstances.fields().forEachRemaining(entry -> { + String key = entry.getKey(); + JsonNode value = entry.getValue(); + if (value.isObject()) { + MaterialInstance materialInstance = createMaterialInstanceComponent(value, name); + builder.materialInstance(key, materialInstance); + } + }); + } + } + + if (node.has("placement_filter")) { + JsonNode placementFilter = node.get("placement_filter"); + if (placementFilter.isObject()) { + if (placementFilter.has("conditions")) { + JsonNode conditions = placementFilter.get("conditions"); + if (conditions.isArray()) { + List filter = createPlacementFilterComponent(conditions); + builder.placementFilter(filter); + } + } + } + } + + // Tags can be applied so that blocks will match return true when queried for the tag + // Potentially useful for resource pack creators + // Ideally we could programmatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html + // This would let us automatically apply the correct vanilla tags to blocks + // However, its worth noting that vanilla tools do not currently honor these tags anyway + if (node.get("tags") instanceof ArrayNode tags) { + Set tagsSet = new ObjectOpenHashSet<>(); + tags.forEach(tag -> tagsSet.add(tag.asText())); + builder.tags(tagsSet); + } + + return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent); + } + + /** + * Creates a {@link BoxComponent} based on a Java block's collision with provided bounds and offsets + * + * @param javaId the block's Java ID + * @param heightTranslation the height translation of the box + * @return the {@link BoxComponent} + */ + private BoxComponent createBoxComponent(int javaId, float heightTranslation) { + // Some blocks (e.g. plants) have no collision box + BlockCollision blockCollision = BlockUtils.getCollision(javaId); + if (blockCollision == null || blockCollision.getBoundingBoxes().length == 0) { + return BoxComponent.emptyBox(); + } + + float minX = 5; + float minY = 5; + float minZ = 5; + float maxX = -5; + float maxY = -5; + float maxZ = -5; + for (BoundingBox boundingBox : blockCollision.getBoundingBoxes()) { + double offsetX = boundingBox.getSizeX() * 0.5; + double offsetY = boundingBox.getSizeY() * 0.5; + double offsetZ = boundingBox.getSizeZ() * 0.5; + + minX = Math.min(minX, (float) (boundingBox.getMiddleX() - offsetX)); + minY = Math.min(minY, (float) (boundingBox.getMiddleY() - offsetY)); + minZ = Math.min(minZ, (float) (boundingBox.getMiddleZ() - offsetZ)); + + maxX = Math.max(maxX, (float) (boundingBox.getMiddleX() + offsetX)); + maxY = Math.max(maxY, (float) (boundingBox.getMiddleY() + offsetY)); + maxZ = Math.max(maxZ, (float) (boundingBox.getMiddleZ() + offsetZ)); + } + minX = MathUtils.clamp(minX, 0, 1); + minY = MathUtils.clamp(minY + heightTranslation, 0, 1); + minZ = MathUtils.clamp(minZ, 0, 1); + maxX = MathUtils.clamp(maxX, 0, 1); + maxY = MathUtils.clamp(maxY + heightTranslation, 0, 1); + maxZ = MathUtils.clamp(maxZ, 0, 1); + + return new BoxComponent( + 16 * (1 - maxX) - 8, // For some odd reason X is mirrored on Bedrock + 16 * minY, + 16 * minZ - 8, + 16 * (maxX - minX), + 16 * (maxY - minY), + 16 * (maxZ - minZ) + ); + } + + /** + * Creates a {@link BoxComponent} based on a Java block's collision + * + * @param javaId the block's Java ID + * @return the {@link BoxComponent} + */ + private BoxComponent createBoxComponent(int javaId) { + return createBoxComponent(javaId, 0); + } + + /** + * Creates the {@link BoxComponent} for an extended collision box based on a Java block's collision + * + * @param javaId the block's Java ID + * @return the {@link BoxComponent} or null if the block's collision box would not exceed 16 y units + */ + private BoxComponent createExtendedBoxComponent(int javaId) { + BlockCollision blockCollision = BlockUtils.getCollision(javaId); + if (blockCollision == null) { + return null; + } + for (BoundingBox box : blockCollision.getBoundingBoxes()) { + double maxY = 0.5 * box.getSizeY() + box.getMiddleY(); + if (maxY > 1) { + return createBoxComponent(javaId, -1); + } + } + return null; + } + + /** + * Creates a {@link BoxComponent} from a JSON Node + * + * @param node the JSON node + * @return the {@link BoxComponent} + */ + private BoxComponent createBoxComponent(JsonNode node) { + if (node != null && node.isObject()) { + if (node.has("origin") && node.has("size")) { + JsonNode origin = node.get("origin"); + float originX = origin.get(0).floatValue(); + float originY = origin.get(1).floatValue(); + float originZ = origin.get(2).floatValue(); + + JsonNode size = node.get("size"); + float sizeX = size.get(0).floatValue(); + float sizeY = size.get(1).floatValue(); + float sizeZ = size.get(2).floatValue(); + + return new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ); + } + } + return null; + } + + /** + * Creates the {@link MaterialInstance} for the passed material instance node and custom block name + * The name is used as a fallback if no texture is provided by the node + * + * @param node the material instance node + * @param name the custom block name + * @return the {@link MaterialInstance} + */ + private MaterialInstance createMaterialInstanceComponent(JsonNode node, String name) { + // Set default values, and use what the user provides if they have provided something + String texture = name; + if (node.has("texture")) { + texture = node.get("texture").asText(); + } + + String renderMethod = "opaque"; + if (node.has("render_method")) { + renderMethod = node.get("render_method").asText(); + } + + boolean faceDimming = true; + if (node.has("face_dimming")) { + faceDimming = node.get("face_dimming").asBoolean(); + } + + boolean ambientOcclusion = true; + if (node.has("ambient_occlusion")) { + ambientOcclusion = node.get("ambient_occlusion").asBoolean(); + } + + return new MaterialInstanceBuilder() + .texture(texture) + .renderMethod(renderMethod) + .faceDimming(faceDimming) + .ambientOcclusion(ambientOcclusion) + .build(); + } + + /** + * Creates the list of {@link PlacementConditions} for the passed conditions node + * + * @param node the conditions node + * @return the list of {@link PlacementConditions} + */ + private List createPlacementFilterComponent(JsonNode node) { + List conditions = new ArrayList<>(); + + // The structure of the placement filter component is the most complex of the current components + // Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries + node.forEach(condition -> { + Set faces = EnumSet.noneOf(Face.class); + if (condition.has("allowed_faces")) { + JsonNode allowedFaces = condition.get("allowed_faces"); + if (allowedFaces.isArray()) { + allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase()))); + } + } + + LinkedHashMap blockFilters = new LinkedHashMap<>(); + if (condition.has("block_filter")) { + JsonNode blockFilter = condition.get("block_filter"); + if (blockFilter.isArray()) { + blockFilter.forEach(filter -> { + if (filter.isObject()) { + if (filter.has("tags")) { + JsonNode tags = filter.get("tags"); + blockFilters.put(tags.asText(), BlockFilterType.TAG); + } + } else if (filter.isTextual()) { + blockFilters.put(filter.asText(), BlockFilterType.BLOCK); + } + }); + } + } + + conditions.add(new PlacementConditions(faces, blockFilters)); + }); + + return conditions; + } + + /** + * Splits the given java state identifier into an array of property=value pairs + * + * @param state the java state identifier + * @return the array of property=value pairs + */ + private String[] splitStateString(String state) { + int openBracketIndex = state.indexOf("["); + + String states = state.substring(openBracketIndex + 1, state.length() - 1); + return states.split(","); + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index 8e30143e8..ce6cec075 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -30,13 +30,19 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Interner; import com.google.common.collect.Interners; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.*; import org.cloudburstmc.nbt.*; import org.cloudburstmc.protocol.bedrock.codec.v582.Bedrock_v582; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589; import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.physics.PistonBehavior; import org.geysermc.geyser.registry.BlockRegistries; @@ -47,6 +53,7 @@ import org.geysermc.geyser.util.BlockUtils; import java.io.DataInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.BiFunction; import java.util.zip.GZIPInputStream; @@ -55,15 +62,35 @@ import java.util.zip.GZIPInputStream; * Populates the block registries. */ public final class BlockRegistryPopulator { + /** + * The stage of population + */ + public enum Stage { + PRE_INIT, + INIT_JAVA, + INIT_BEDROCK, + POST_INIT; + } + + public static void populate(Stage stage) { + switch (stage) { + case PRE_INIT -> { nullifyBlocksNode(); } + case INIT_JAVA -> { registerJavaBlocks(); } + case INIT_BEDROCK -> { registerBedrockBlocks(); } + case POST_INIT -> { nullifyBlocksNode(); } + default -> { throw new IllegalArgumentException("Unknown stage: " + stage); } + } + } + /** * Stores the raw blocks JSON until it is no longer needed. */ private static JsonNode BLOCKS_JSON; + private static int minCustomRuntimeID = -1; + private static int maxCustomRuntimeID = -1; + private static int javaBlocksSize = -1; - public static void populate() { - registerJavaBlocks(); - registerBedrockBlocks(); - + private static void nullifyBlocksNode() { BLOCKS_JSON = null; } @@ -154,36 +181,83 @@ public final class BlockRegistryPopulator { Interner statesInterner = Interners.newStrongInterner(); for (Map.Entry, BiFunction> palette : blockMappers.entrySet()) { - NbtList blocksTag; + int protocolVersion = palette.getKey().valueInt(); + List vanillaBlockStates; + List blockStates; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(String.format("bedrock/block_palette.%s.nbt", palette.getKey().key())); - NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) { + NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) { NbtMap blockPalette = (NbtMap) nbtInputStream.readTag(); - blocksTag = (NbtList) blockPalette.getList("blocks", NbtType.COMPOUND); + + vanillaBlockStates = new ArrayList<>(blockPalette.getList("blocks", NbtType.COMPOUND)); + for (int i = 0; i < vanillaBlockStates.size(); i++) { + NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder(); + builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 + builder.remove("network_id"); // Added in 1.19.80 - ???? + builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); + vanillaBlockStates.set(i, builder.build()); + } + + blockStates = new ArrayList<>(vanillaBlockStates); } catch (Exception e) { throw new AssertionError("Unable to get blocks from runtime block states", e); } + int stateVersion = vanillaBlockStates.get(0).getInt("version"); + + List customBlockProperties = new ArrayList<>(); + List customBlockStates = new ArrayList<>(); + List customExtBlockStates = new ArrayList<>(); + int[] remappedVanillaIds = new int[0]; + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) { + customBlockProperties.add(CustomBlockRegistryPopulator.generateBlockPropertyData(customBlock, protocolVersion)); + CustomBlockRegistryPopulator.generateCustomBlockStates(customBlock, customBlockStates, customExtBlockStates, stateVersion); + } + blockStates.addAll(customBlockStates); + GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states to v" + protocolVersion + " palette."); + + // The palette is sorted by the FNV1 64-bit hash of the name + blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name")))); + } + // New since 1.16.100 - find the block runtime ID by the order given to us in the block palette, // as we no longer send a block palette - Object2ObjectMap blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blocksTag.size()); - GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blocksTag.size()]; - - int stateVersion = -1; - for (int i = 0; i < blocksTag.size(); i++) { - NbtMapBuilder builder = blocksTag.get(i).toBuilder(); - builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 - builder.remove("network_id"); // Added in 1.19.80 - ???? - builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); - NbtMap tag = builder.build(); + Object2ObjectMap blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blockStates.size()); + GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blockStates.size()]; + for (int i = 0; i < blockStates.size(); i++) { + NbtMap tag = blockStates.get(i); if (blockStateOrderedMap.containsKey(tag)) { throw new AssertionError("Duplicate block states in Bedrock palette: " + tag); } GeyserBedrockBlock block = new GeyserBedrockBlock(i, tag); blockStateOrderedMap.put(tag, block); bedrockRuntimeMap[i] = block; - if (stateVersion == -1) { - stateVersion = tag.getInt("version"); + } + + Object2ObjectMap customBlockStateDefinitions = Object2ObjectMaps.emptyMap(); + Int2ObjectMap extendedCollisionBoxes = new Int2ObjectOpenHashMap<>(); + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + customBlockStateDefinitions = new Object2ObjectOpenHashMap<>(customExtBlockStates.size()); + for (int i = 0; i < customExtBlockStates.size(); i++) { + NbtMap tag = customBlockStates.get(i); + CustomBlockState blockState = customExtBlockStates.get(i); + GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(tag); + customBlockStateDefinitions.put(blockState, bedrockBlock); + + Set extendedCollisionjavaIds = BlockRegistries.EXTENDED_COLLISION_BOXES.getOrDefault(blockState.block(), null); + if (extendedCollisionjavaIds != null) { + for (int javaId : extendedCollisionjavaIds) { + extendedCollisionBoxes.put(javaId, bedrockBlock); + } + } + } + + remappedVanillaIds = new int[vanillaBlockStates.size()]; + for (int i = 0; i < vanillaBlockStates.size(); i++) { + GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(vanillaBlockStates.get(i)); + remappedVanillaIds[i] = bedrockBlock != null ? bedrockBlock.getRuntimeId() : -1; } } + int javaRuntimeId = -1; GeyserBedrockBlock airDefinition = null; @@ -194,7 +268,8 @@ public final class BlockRegistryPopulator { BiFunction stateMapper = blockMappers.getOrDefault(palette.getKey(), emptyMapper); - GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[BLOCKS_JSON.size()]; + GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize]; + GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize]; Map flowerPotBlocks = new Object2ObjectOpenHashMap<>(); Map itemFrames = new Object2ObjectOpenHashMap<>(); @@ -206,11 +281,22 @@ public final class BlockRegistryPopulator { javaRuntimeId++; Map.Entry entry = blocksIterator.next(); String javaId = entry.getKey(); + GeyserBedrockBlock vanillaBedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper)); - GeyserBedrockBlock bedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper)); - if (bedrockDefinition == null) { - throw new RuntimeException("Unable to find " + javaId + " Bedrock BlockDefinition on version " - + palette.getKey().key() + "! Built NBT tag: \n" + buildBedrockState(entry.getValue(), stateVersion, stateMapper)); + GeyserBedrockBlock bedrockDefinition; + CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(javaRuntimeId); + if (blockStateOverride == null) { + bedrockDefinition = vanillaBedrockDefinition; + if (bedrockDefinition == null) { + throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Built NBT tag: \n" + + palette.getKey().key() + buildBedrockState(entry.getValue(), stateVersion, stateMapper)); + } + } else { + bedrockDefinition = customBlockStateDefinitions.get(blockStateOverride); + if (bedrockDefinition == null) { + throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Custom block override: \n" + + blockStateOverride); + } } switch (javaId) { @@ -236,9 +322,10 @@ public final class BlockRegistryPopulator { // Get the tag needed for non-empty flower pots if (entry.getValue().get("pottable") != null) { - flowerPotBlocks.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockDefinition.getRuntimeId())); + flowerPotBlocks.put(cleanJavaIdentifier.intern(), blockStates.get(bedrockDefinition.getRuntimeId())); } + javaToVanillaBedrockBlocks[javaRuntimeId] = vanillaBedrockDefinition; javaToBedrockBlocks[javaRuntimeId] = bedrockDefinition; } @@ -263,6 +350,33 @@ public final class BlockRegistryPopulator { } builder.bedrockMovingBlock(movingBlockDefinition); + Map nonVanillaStateOverrides = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get(); + if (nonVanillaStateOverrides.size() > 0) { + // First ensure all non vanilla runtime IDs at minimum are air in case they aren't consecutive + Arrays.fill(javaToVanillaBedrockBlocks, minCustomRuntimeID, javaToVanillaBedrockBlocks.length, airDefinition); + Arrays.fill(javaToBedrockBlocks, minCustomRuntimeID, javaToBedrockBlocks.length, airDefinition); + + for (Map.Entry entry : nonVanillaStateOverrides.entrySet()) { + GeyserBedrockBlock bedrockDefinition = customBlockStateDefinitions.get(entry.getValue()); + if (bedrockDefinition == null) { + GeyserImpl.getInstance().getLogger().warning("Unable to find custom block for " + entry.getValue()); + continue; + } + + JavaBlockState javaState = entry.getKey(); + int stateRuntimeId = javaState.javaId(); + + boolean waterlogged = javaState.waterlogged(); + + if (waterlogged) { + BlockRegistries.WATERLOGGED.register(set -> set.set(stateRuntimeId)); + } + + javaToVanillaBedrockBlocks[stateRuntimeId] = bedrockDefinition; // TODO: Check this? + javaToBedrockBlocks[stateRuntimeId] = bedrockDefinition; + } + } + // Loop around again to find all item frame runtime IDs Object2ObjectMaps.fastForEach(blockStateOrderedMap, entry -> { String name = entry.getKey().getString("name"); @@ -274,10 +388,15 @@ public final class BlockRegistryPopulator { BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion) .bedrockRuntimeMap(bedrockRuntimeMap) .javaToBedrockBlocks(javaToBedrockBlocks) + .javaToVanillaBedrockBlocks(javaToVanillaBedrockBlocks) .stateDefinitionMap(blockStateOrderedMap) .itemFrames(itemFrames) .flowerPotBlocks(flowerPotBlocks) .jigsawStates(jigsawDefinitions) + .remappedVanillaIds(remappedVanillaIds) + .blockProperties(customBlockProperties) + .customBlockStateDefinitions(customBlockStateDefinitions) + .extendedCollisionBoxes(extendedCollisionBoxes) .build()); } } @@ -290,7 +409,20 @@ public final class BlockRegistryPopulator { throw new AssertionError("Unable to load Java block mappings", e); } - BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[blocksJson.size()]); // Set array size to number of blockstates + javaBlocksSize = blocksJson.size(); + + if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) { + minCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().min(Comparator.comparing(JavaBlockState::javaId)).get().javaId(); + maxCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().max(Comparator.comparing(JavaBlockState::javaId)).get().javaId(); + + if (minCustomRuntimeID < blocksJson.size()) { + throw new RuntimeException("Non vanilla custom block state overrides runtime ID must start after the last vanilla block state (" + javaBlocksSize + ")"); + } + + javaBlocksSize = maxCustomRuntimeID + 1; // Runtime ids start at 0, so we need to add 1 + } + + BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[javaBlocksSize]); // Set array size to number of blockstates Deque cleanIdentifiers = new ArrayDeque<>(); @@ -428,6 +560,46 @@ public final class BlockRegistryPopulator { } BlockStateValues.JAVA_WATER_ID = waterRuntimeId; + if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) { + Set usedNonVanillaRuntimeIDs = new HashSet<>(); + + for (JavaBlockState javaBlockState : BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet()) { + if (!usedNonVanillaRuntimeIDs.add(javaBlockState.javaId())) { + throw new RuntimeException("Duplicate runtime ID " + javaBlockState.javaId() + " for non vanilla Java block state " + javaBlockState.identifier()); + } + + CustomBlockState customBlockState = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().get(javaBlockState); + + String javaId = javaBlockState.identifier(); + int stateRuntimeId = javaBlockState.javaId(); + BlockMapping blockMapping = BlockMapping.builder() + .canBreakWithHand(javaBlockState.canBreakWithHand()) + .pickItem(javaBlockState.pickItem()) + .isNonVanilla(true) + .javaIdentifier(javaId) + .javaBlockId(javaBlockState.stateGroupId()) + .hardness(javaBlockState.blockHardness()) + .pistonBehavior(javaBlockState.pistonBehavior() == null ? PistonBehavior.NORMAL : PistonBehavior.getByName(javaBlockState.pistonBehavior())) + .isBlockEntity(javaBlockState.hasBlockEntity()) + .build(); + + String cleanJavaIdentifier = BlockUtils.getCleanIdentifier(javaBlockState.identifier()); + String bedrockIdentifier = customBlockState.block().identifier(); + + if (!cleanJavaIdentifier.equals(cleanIdentifiers.peekLast())) { + uniqueJavaId++; + cleanIdentifiers.add(cleanJavaIdentifier.intern()); + } + + BlockRegistries.JAVA_IDENTIFIER_TO_ID.register(javaId, stateRuntimeId); + BlockRegistries.JAVA_BLOCKS.register(stateRuntimeId, blockMapping); + + // Keeping this here since this is currently unchanged between versions + // It's possible to only have this store differences in names, but the key set of all Java names is used in sending command suggestions + BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.register(cleanJavaIdentifier.intern(), bedrockIdentifier.intern()); + } + } + BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0])); BLOCKS_JSON = blocksJson; @@ -481,4 +653,22 @@ public final class BlockRegistryPopulator { tagBuilder.put("states", statesBuilder.build()); return tagBuilder.build(); } + + private static final long FNV1_64_OFFSET_BASIS = 0xcbf29ce484222325L; + private static final long FNV1_64_PRIME = 1099511628211L; + + /** + * Hashes a string using the FNV-1a 64-bit algorithm. + * + * @param str The string to hash + * @return The hashed string + */ + private static long fnv164(String str) { + long hash = FNV1_64_OFFSET_BASIS; + for (byte b : str.getBytes(StandardCharsets.UTF_8)) { + hash *= FNV1_64_PRIME; + hash ^= b; + } + return hash; + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java new file mode 100644 index 000000000..5d453582c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java @@ -0,0 +1,480 @@ +package org.geysermc.geyser.registry.populator; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomBlocksEvent; +import org.geysermc.geyser.api.util.CreativeCategory; +import org.geysermc.geyser.level.block.GeyserCustomBlockState; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder; +import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.mappings.MappingsConfigReader; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.util.MathUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CustomBlockRegistryPopulator { + /** + * The stage of population + */ + public enum Stage { + DEFINITION, + VANILLA_REGISTRATION, + NON_VANILLA_REGISTRATION, + CUSTOM_REGISTRATION; + } + + /** + * Populates the custom block registries by stage + * + * @param stage the stage to populate + */ + public static void populate(Stage stage) { + if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + return; + } + + switch (stage) { + case DEFINITION -> { populateBedrock(); } + case VANILLA_REGISTRATION -> { populateVanilla(); } + case NON_VANILLA_REGISTRATION -> { populateNonVanilla(); } + case CUSTOM_REGISTRATION -> { registration(); } + default -> { throw new IllegalArgumentException("Unknown stage: " + stage); } + } + } + + private static Set customBlocks; + private static Set customBlockNames; + private static Int2ObjectMap blockStateOverrides; + private static Map customBlockItemOverrides; + private static Map nonVanillaBlockStateOverrides; + + /** + * Initializes custom blocks defined by API + */ + private static void populateBedrock() { + customBlocks = new ObjectOpenHashSet<>(); + customBlockNames = new ObjectOpenHashSet<>(); + blockStateOverrides = new Int2ObjectOpenHashMap<>(); + customBlockItemOverrides = new HashMap<>(); + nonVanillaBlockStateOverrides = new HashMap<>(); + + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomBlocksEvent() { + @Override + public void register(@NonNull CustomBlockData customBlockData) { + if (customBlockData.name().length() == 0) { + throw new IllegalArgumentException("Custom block name must have at least 1 character."); + } + if (!customBlockNames.add(customBlockData.name())) { + throw new IllegalArgumentException("Another custom block was already registered under the name: " + customBlockData.name()); + } + if (Character.isDigit(customBlockData.name().charAt(0))) { + throw new IllegalArgumentException("Custom block can not start with a digit. Name: " + customBlockData.name()); + } + customBlocks.add(customBlockData); + } + + @Override + public void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState) { + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1); + if (id == -1) { + throw new IllegalArgumentException("Unknown Java block state. Identifier: " + javaIdentifier); + } + if (!customBlocks.contains(customBlockState.block())) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name()); + } + CustomBlockState oldBlockState = blockStateOverrides.put(id, customBlockState); + if (oldBlockState != null) { + GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " + + javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name()); + } + } + + @Override + public void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData) { + if (!customBlocks.contains(customBlockData)) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockData.name()); + } + customBlockItemOverrides.put(javaIdentifier, customBlockData); + } + + @Override + public void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState) { + if (!customBlocks.contains(customBlockState.block())) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name()); + } + nonVanillaBlockStateOverrides.put(javaBlockState, customBlockState); + } + }); + } + + /** + * Registers all vanilla custom blocks and skulls defined by API and mappings + */ + private static void populateVanilla() { + for (CustomSkull customSkull : BlockRegistries.CUSTOM_SKULLS.get().values()) { + customBlocks.add(customSkull.getCustomBlockData()); + } + + Map> extendedCollisionBoxes = new HashMap<>(); + Map extendedCollisionBoxSet = new HashMap<>(); + MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); + mappingsConfigReader.loadBlockMappingsFromJson((key, block) -> { + customBlocks.add(block.data()); + if (block.overrideItem()) { + customBlockItemOverrides.put(block.javaIdentifier(), block.data()); + } + block.states().forEach((javaIdentifier, customBlockState) -> { + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1); + blockStateOverrides.put(id, customBlockState.state()); + BoxComponent extendedCollisionBox = customBlockState.extendedCollisionBox(); + if (extendedCollisionBox != null) { + CustomBlockData extendedCollisionBlock = extendedCollisionBoxSet.computeIfAbsent(extendedCollisionBox, box -> { + CustomBlockData collisionBlock = createExtendedCollisionBlock(box, extendedCollisionBoxSet.size()); + customBlocks.add(collisionBlock); + return collisionBlock; + }); + extendedCollisionBoxes.computeIfAbsent(extendedCollisionBlock, k -> new HashSet<>()) + .add(id); + } + }); + }); + + BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + blockStateOverrides.size() + " custom block overrides."); + + BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(customBlockItemOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + customBlockItemOverrides.size() + " custom block item overrides."); + + BlockRegistries.EXTENDED_COLLISION_BOXES.set(extendedCollisionBoxes); + GeyserImpl.getInstance().getLogger().info("Registered " + extendedCollisionBoxes.size() + " custom block extended collision boxes."); + } + + /** + * Registers all non-vanilla custom blocks defined by API + */ + private static void populateNonVanilla() { + BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.set(nonVanillaBlockStateOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + nonVanillaBlockStateOverrides.size() + " non-vanilla block overrides."); + } + + /** + * Registers all bedrock custom blocks defined in previous stages + */ + private static void registration() { + BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0])); + GeyserImpl.getInstance().getLogger().info("Registered " + customBlocks.size() + " custom blocks."); + } + + /** + * Generates and appends all custom block states to the provided list of custom block states + * Appends the custom block states to the provided list of NBT maps + * + * @param customBlock the custom block data to generate states for + * @param blockStates the list of NBT maps to append the custom block states to + * @param customExtBlockStates the list of custom block states to append the custom block states to + * @param stateVersion the state version to use for the custom block states + */ + static void generateCustomBlockStates(CustomBlockData customBlock, List blockStates, List customExtBlockStates, int stateVersion) { + int totalPermutations = 1; + for (CustomBlockProperty property : customBlock.properties().values()) { + totalPermutations *= property.values().size(); + } + + for (int i = 0; i < totalPermutations; i++) { + NbtMapBuilder statesBuilder = NbtMap.builder(); + int permIndex = i; + for (CustomBlockProperty property : customBlock.properties().values()) { + statesBuilder.put(property.name(), property.values().get(permIndex % property.values().size())); + permIndex /= property.values().size(); + } + NbtMap states = statesBuilder.build(); + + blockStates.add(NbtMap.builder() + .putString("name", customBlock.identifier()) + .putInt("version", stateVersion) + .putCompound("states", states) + .build()); + customExtBlockStates.add(new GeyserCustomBlockState(customBlock, states)); + } + } + + /** + * Generates and returns the block property data for the provided custom block + * + * @param customBlock the custom block to generate block property data for + * @param protocolVersion the protocol version to use for the block property data + * @return the block property data for the provided custom block + */ + @SuppressWarnings("unchecked") + static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock, int protocolVersion) { + List permutations = new ArrayList<>(); + for (CustomBlockPermutation permutation : customBlock.permutations()) { + permutations.add(NbtMap.builder() + .putCompound("components", CustomBlockRegistryPopulator.convertComponents(permutation.components(), protocolVersion)) + .putString("condition", permutation.condition()) + .build()); + } + + // The order that properties are defined influences the order that block states are generated + List properties = new ArrayList<>(); + for (CustomBlockProperty property : customBlock.properties().values()) { + NbtMapBuilder propertyBuilder = NbtMap.builder() + .putString("name", property.name()); + if (property.type() == PropertyType.booleanProp()) { + propertyBuilder.putList("enum", NbtType.BYTE, List.of((byte) 0, (byte) 1)); + } else if (property.type() == PropertyType.integerProp()) { + propertyBuilder.putList("enum", NbtType.INT, (List) property.values()); + } else if (property.type() == PropertyType.stringProp()) { + propertyBuilder.putList("enum", NbtType.STRING, (List) property.values()); + } + properties.add(propertyBuilder.build()); + } + + CreativeCategory creativeCategory = customBlock.creativeCategory() != null ? customBlock.creativeCategory() : CreativeCategory.NONE; + String creativeGroup = customBlock.creativeGroup() != null ? customBlock.creativeGroup() : ""; + NbtMap propertyTag = NbtMap.builder() + .putCompound("components", CustomBlockRegistryPopulator.convertComponents(customBlock.components(), protocolVersion)) + // this is required or the client will crash + // in the future, this can be used to replace items in the creative inventory + // this would require us to map https://wiki.bedrock.dev/documentation/creative-categories.html#for-blocks programatically + .putCompound("menu_category", NbtMap.builder() + .putString("category", creativeCategory.internalName()) + .putString("group", creativeGroup) + .putBoolean("is_hidden_in_commands", false) + .build()) + // meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically + .putInt("molangVersion", 1) + .putList("permutations", NbtType.COMPOUND, permutations) + .putList("properties", NbtType.COMPOUND, properties) + .build(); + return new BlockPropertyData(customBlock.identifier(), propertyTag); + } + + /** + * Converts the provided custom block components to an {@link NbtMap} to be sent to the client in the StartGame packet + * + * @param components the custom block components to convert + * @param protocolVersion the protocol version to use for the conversion + * @return the NBT representation of the provided custom block components + */ + private static NbtMap convertComponents(CustomBlockComponents components, int protocolVersion) { + if (components == null) { + return NbtMap.EMPTY; + } + + NbtMapBuilder builder = NbtMap.builder(); + if (components.displayName() != null) { + builder.putCompound("minecraft:display_name", NbtMap.builder() + .putString("value", components.displayName()) + .build()); + } + + if (components.selectionBox() != null) { + builder.putCompound("minecraft:selection_box", convertBox(components.selectionBox())); + } + + if (components.collisionBox() != null) { + builder.putCompound("minecraft:collision_box", convertBox(components.collisionBox())); + } + + if (components.geometry() != null) { + NbtMapBuilder geometryBuilder = NbtMap.builder(); + if (protocolVersion >= Bedrock_v594.CODEC.getProtocolVersion()) { + geometryBuilder.putString("identifier", components.geometry().identifier()); + if (components.geometry().boneVisibility() != null) { + NbtMapBuilder boneVisibilityBuilder = NbtMap.builder(); + components.geometry().boneVisibility().entrySet().forEach( + entry -> boneVisibilityBuilder.putString(entry.getKey(), entry.getValue())); + geometryBuilder.putCompound("bone_visibility", boneVisibilityBuilder.build()); + } + } else { + geometryBuilder.putString("value", components.geometry().identifier()); + } + builder.putCompound("minecraft:geometry", geometryBuilder.build()); + } + + if (!components.materialInstances().isEmpty()) { + NbtMapBuilder materialsBuilder = NbtMap.builder(); + for (Map.Entry entry : components.materialInstances().entrySet()) { + MaterialInstance materialInstance = entry.getValue(); + materialsBuilder.putCompound(entry.getKey(), NbtMap.builder() + .putString("texture", materialInstance.texture()) + .putString("render_method", materialInstance.renderMethod()) + .putBoolean("face_dimming", materialInstance.faceDimming()) + .putBoolean("ambient_occlusion", materialInstance.faceDimming()) + .build()); + } + + builder.putCompound("minecraft:material_instances", NbtMap.builder() + // we could read these, but there is no functional reason to use them at the moment + // they only allow you to make aliases for material instances + // but you could already just define the same instance twice if this was really needed + .putCompound("mappings", NbtMap.EMPTY) + .putCompound("materials", materialsBuilder.build()) + .build()); + } + + if (components.placementFilter() != null) { + builder.putCompound("minecraft:placement_filter", NbtMap.builder() + .putList("conditions", NbtType.COMPOUND, convertPlacementFilter(components.placementFilter())) + .build()); + } + + if (components.destructibleByMining() != null) { + builder.putCompound("minecraft:destructible_by_mining", NbtMap.builder() + .putFloat("value", components.destructibleByMining()) + .build()); + } + + if (components.friction() != null) { + builder.putCompound("minecraft:friction", NbtMap.builder() + .putFloat("value", components.friction()) + .build()); + } + + if (components.lightEmission() != null) { + builder.putCompound("minecraft:light_emission", NbtMap.builder() + .putByte("emission", components.lightEmission().byteValue()) + .build()); + } + + if (components.lightDampening() != null) { + builder.putCompound("minecraft:light_dampening", NbtMap.builder() + .putByte("lightLevel", components.lightDampening().byteValue()) + .build()); + } + + if (components.transformation() != null) { + builder.putCompound("minecraft:transformation", NbtMap.builder() + .putInt("RX", MathUtils.unwrapDegreesToInt(components.transformation().rx()) / 90) + .putInt("RY", MathUtils.unwrapDegreesToInt(components.transformation().ry()) / 90) + .putInt("RZ", MathUtils.unwrapDegreesToInt(components.transformation().rz()) / 90) + .putFloat("SX", components.transformation().sx()) + .putFloat("SY", components.transformation().sy()) + .putFloat("SZ", components.transformation().sz()) + .putFloat("TX", components.transformation().tx()) + .putFloat("TY", components.transformation().ty()) + .putFloat("TZ", components.transformation().tz()) + .build()); + } + + if (components.unitCube()) { + builder.putCompound("minecraft:unit_cube", NbtMap.EMPTY); + } + + // place_air is not an actual component + // We just apply a dummy event to prevent the client from trying to place a block + // This mitigates the issue with the client sometimes double placing blocks + if (components.placeAir()) { + builder.putCompound("minecraft:on_player_placing", NbtMap.builder() + .putString("triggerType", "geyser:place_event") + .build()); + } + + if (!components.tags().isEmpty()) { + components.tags().forEach(tag -> builder.putCompound("tag:" + tag, NbtMap.EMPTY)); + } + + return builder.build(); + } + + /** + * Converts the provided box component to an {@link NbtMap} + * + * @param boxComponent the box component to convert + * @return the NBT representation of the provided box component + */ + private static NbtMap convertBox(BoxComponent boxComponent) { + return NbtMap.builder() + .putBoolean("enabled", !boxComponent.isEmpty()) + .putList("origin", NbtType.FLOAT, boxComponent.originX(), boxComponent.originY(), boxComponent.originZ()) + .putList("size", NbtType.FLOAT, boxComponent.sizeX(), boxComponent.sizeY(), boxComponent.sizeZ()) + .build(); + } + + /** + * Converts the provided placement filter to a list of {@link NbtMap} + * + * @param placementFilter the placement filter to convert + * @return the NBT representation of the provided placement filter + */ + private static List convertPlacementFilter(List placementFilter) { + List conditions = new ArrayList<>(); + placementFilter.forEach((condition) -> { + NbtMapBuilder conditionBuilder = NbtMap.builder(); + + // allowed_faces on the network is represented by 6 bits for the 6 possible faces + // the enum has the proper values for that face only, so we just bitwise OR them together + byte allowedFaces = 0; + for (Face face : condition.allowedFaces()) { allowedFaces |= (1 << face.ordinal()); } + conditionBuilder.putByte("allowed_faces", allowedFaces); + + // block_filters is a list of either blocks or queries for block tags + // if these match the block the player is trying to place on, the placement is allowed by the client + List blockFilters = new ArrayList<>(); + condition.blockFilters().forEach((value, type) -> { + NbtMapBuilder blockFilterBuilder = NbtMap.builder(); + switch (type) { + case BLOCK -> blockFilterBuilder.putString("name", value); + // meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically + case TAG -> blockFilterBuilder.putString("tags", value).putInt("tags_version", 6); + } + blockFilters.add(blockFilterBuilder.build()); + }); + conditionBuilder.putList("block_filters", NbtType.COMPOUND, blockFilters); + conditions.add(conditionBuilder.build()); + }); + + return conditions; + } + + private static CustomBlockData createExtendedCollisionBlock(BoxComponent boxComponent, int extendedCollisionBlock) { + CustomBlockData customBlockData = new CustomBlockDataBuilder() + .name("extended_collision_" + extendedCollisionBlock) + .components( + new CustomBlockComponentsBuilder() + .collisionBox(boxComponent) + .selectionBox(BoxComponent.emptyBox()) + .materialInstance("*", new MaterialInstanceBuilder() + .texture("glass") + .renderMethod("alpha_test") + .faceDimming(false) + .ambientOcclusion(false) + .build()) + .lightDampening(0) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.invisible") + .build()) + .build()) + .build(); + return customBlockData; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java index 629fc17c5..3f3f5a4ba 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -41,10 +41,9 @@ import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.event.type.GeyserDefineCustomItemsEventImpl; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.components.ToolBreakSpeedsUtils; import org.geysermc.geyser.item.components.WearableSlot; -import org.geysermc.geyser.item.mappings.MappingsConfigReader; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.mappings.MappingsConfigReader; import org.geysermc.geyser.registry.type.GeyserMappingItem; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.NonVanillaItemRegistration; @@ -56,7 +55,7 @@ public class CustomItemRegistryPopulator { public static void populate(Map items, Multimap customItems, List nonVanillaCustomItems) { MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); // Load custom items from mappings files - mappingsConfigReader.loadMappingsFromJson((key, item) -> { + mappingsConfigReader.loadItemMappingsFromJson((key, item) -> { if (CustomItemRegistryPopulator.initialCheck(key, item, items)) { customItems.get(key).add(item); } @@ -294,34 +293,45 @@ public class CustomItemRegistryPopulator { boolean canDestroyInCreative = true; float miningSpeed = 1.0f; - if (toolType.equals("shears")) { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShearsDigger(15)); - } else { - int toolSpeed = ToolBreakSpeedsUtils.toolTierToSpeed(toolTier); - switch (toolType) { - case "sword" -> { - miningSpeed = 1.5f; - canDestroyInCreative = false; - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getSwordDigger(toolSpeed)); - componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY); - } - case "pickaxe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getPickaxeDigger(toolSpeed, toolTier)); - setItemTag(componentBuilder, "pickaxe"); - } - case "axe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getAxeDigger(toolSpeed)); - setItemTag(componentBuilder, "axe"); - } - case "shovel" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShovelDigger(toolSpeed)); - setItemTag(componentBuilder, "shovel"); - } - case "hoe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getHoeDigger(toolSpeed)); - setItemTag(componentBuilder, "hoe"); - } - } + // This means client side the tool can never destroy a block + // This works because the molang '1' for tags will be true for all blocks and the speed will be 0 + // We want this since we calculate break speed server side in BedrockActionTranslator + List speed = new ArrayList<>(List.of( + NbtMap.builder() + .putCompound("block", NbtMap.builder() + .putString("tags", "1") + .build()) + .putCompound("on_dig", NbtMap.builder() + .putCompound("condition", NbtMap.builder() + .putString("expression", "") + .putInt("version", -1) + .build()) + .putString("event", "tool_durability") + .putString("target", "self") + .build()) + .putInt("speed", 0) + .build() + )); + + componentBuilder.putCompound("minecraft:digger", + NbtMap.builder() + .putList("destroy_speeds", NbtType.COMPOUND, speed) + .putCompound("on_dig", NbtMap.builder() + .putCompound("condition", NbtMap.builder() + .putString("expression", "") + .putInt("version", -1) + .build()) + .putString("event", "tool_durability") + .putString("target", "self") + .build()) + .putBoolean("use_efficiency", true) + .build() + ); + + if (toolType.equals("sword")) { + miningSpeed = 1.5f; + canDestroyInCreative = false; + componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY); } itemProperties.putBoolean("hand_equipped", true); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java new file mode 100644 index 000000000..9c17ca952 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.populator; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.NonNull; +import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomSkullsEvent; +import org.geysermc.geyser.configuration.GeyserCustomSkullConfiguration; +import org.geysermc.geyser.pack.SkullResourcePackManager; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.skin.SkinProvider; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +public class CustomSkullRegistryPopulator { + + public static void populate() { + SkullResourcePackManager.SKULL_SKINS.clear(); // Remove skins after reloading + BlockRegistries.CUSTOM_SKULLS.set(Object2ObjectMaps.emptyMap()); + + if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + return; + } + + GeyserCustomSkullConfiguration skullConfig; + try { + GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); + Path skullConfigPath = bootstrap.getConfigFolder().resolve("custom-skulls.yml"); + File skullConfigFile = FileUtils.fileOrCopiedFromResource(skullConfigPath.toFile(), "custom-skulls.yml", Function.identity(), bootstrap); + skullConfig = FileUtils.loadConfig(skullConfigFile, GeyserCustomSkullConfiguration.class); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), e); + return; + } + + BlockRegistries.CUSTOM_SKULLS.set(new Object2ObjectOpenHashMap<>()); + + List profiles = new ArrayList<>(skullConfig.getPlayerProfiles()); + List usernames = new ArrayList<>(skullConfig.getPlayerUsernames()); + List uuids = new ArrayList<>(skullConfig.getPlayerUUIDs()); + List skinHashes = new ArrayList<>(skullConfig.getPlayerSkinHashes()); + + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomSkullsEvent() { + @Override + public void register(@NonNull String texture, @NonNull SkullTextureType type) { + switch (type) { + case USERNAME -> usernames.add(texture); + case UUID -> uuids.add(texture); + case PROFILE -> profiles.add(texture); + case SKIN_HASH -> skinHashes.add(texture); + } + } + }); + + usernames.forEach((username) -> { + String profile = getProfileFromUsername(username); + if (profile != null) { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + } + }); + + uuids.forEach((uuid) -> { + String profile = getProfileFromUuid(uuid); + if (profile != null) { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + } + }); + + profiles.forEach((profile) -> { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + }); + + skinHashes.forEach((skinHash) -> { + if (!skinHash.matches("^[a-fA-F0-9]+$")) { + GeyserImpl.getInstance().getLogger().error("Skin hash " + skinHash + " does not match required format ^[a-fA-F0-9]{64}$ and will not be added as a custom block."); + return; + } + + try { + SkullResourcePackManager.cacheSkullSkin(skinHash); + BlockRegistries.CUSTOM_SKULLS.register(skinHash, new CustomSkull(skinHash)); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to cache skin for skull texture " + skinHash + " This skull will not be added as a custom block.", e); + } + }); + + GeyserImpl.getInstance().getLogger().info("Registered " + BlockRegistries.CUSTOM_SKULLS.get().size() + " custom skulls as custom blocks."); + } + + /** + * Gets the skin hash from a base64 encoded profile + * @param profile the base64 encoded profile + * @return the skin hash or null if the profile is invalid + */ + private static String getSkinHash(String profile) { + try { + SkinManager.GameProfileData profileData = SkinManager.GameProfileData.loadFromJson(profile); + if (profileData == null) { + GeyserImpl.getInstance().getLogger().warning("Skull texture " + profile + " contained no skins and will not be added as a custom block."); + return null; + } + String skinUrl = profileData.skinUrl(); + return skinUrl.substring(skinUrl.lastIndexOf("/") + 1); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Skull texture " + profile + " is invalid and will not be added as a custom block.", e); + return null; + } + } + + /** + * Gets the base64 encoded profile from a player's username + * @param username the player username + * @return the base64 encoded profile or null if the request failed + */ + private static String getProfileFromUsername(String username) { + try { + return SkinProvider.requestTexturesFromUsername(username).get(); + } catch (InterruptedException | ExecutionException e) { + GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + username + " This skull will not be added as a custom block.", e); + return null; + } + } + + /** + * Gets the base64 encoded profile from a player's UUID + * @param uuid the player UUID + * @return the base64 encoded profile or null if the request failed + */ + private static String getProfileFromUuid(String uuid) { + try { + String uuidDigits = uuid.replace("-", ""); + if (uuidDigits.length() != 32) { + GeyserImpl.getInstance().getLogger().error("Invalid skull uuid " + uuid + " This skull will not be added as a custom block."); + return null; + } + return SkinProvider.requestTexturesFromUUID(uuid).get(); + } catch (InterruptedException | ExecutionException e) { + GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + uuid + " This skull will not be added as a custom block.", e); + return null; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index b0d88c3e7..f243d358a 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -34,6 +34,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.objects.*; +import org.geysermc.geyser.Constants; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -49,15 +50,18 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.type.*; import java.io.InputStream; @@ -176,6 +180,8 @@ public class ItemRegistryPopulator { Object2ObjectMap bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>(); Object2IntMap blacklistedIdentifiers = new Object2IntOpenHashMap<>(); + Object2ObjectMap customBlockItemDefinitions = new Object2ObjectOpenHashMap<>(); + List buckets = new ObjectArrayList<>(); List carpets = new ObjectArrayList<>(); @@ -249,15 +255,29 @@ public class ItemRegistryPopulator { BlockDefinition bedrockBlock = null; Integer firstBlockRuntimeId = entry.getValue().getFirstBlockRuntimeId(); + BlockDefinition customBlockItemOverride = null; if (firstBlockRuntimeId != null) { BlockDefinition blockOverride = bedrockBlockIdOverrides.get(bedrockIdentifier); - if (blockOverride != null) { + + // We'll do this here for custom blocks we want in the creative inventory so we can piggyback off the existing logic to find these + // blocks in creativeItems + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(javaItem.javaIdentifier(), null); + if (customBlockData != null) { + // this block has a custom item override and thus we should use its runtime ID for the ItemMapping + if (customBlockData.includedInCreativeInventory()) { + CustomBlockState customBlockState = customBlockData.defaultBlockState(); + customBlockItemOverride = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlockState, null); + } + } + + // If it' s a custom block we can't do this because we need to make sure we find the creative item + if (blockOverride != null && customBlockItemOverride == null) { // Straight from BDS is our best chance of getting an item that doesn't run into issues bedrockBlock = blockOverride; } else { // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining - int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, -1); - if (aValidBedrockBlockId == -1) { + int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1); + if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) { // Fallback bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId); } else { @@ -273,7 +293,7 @@ public class ItemRegistryPopulator { // and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) { - GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getBedrockBlock(i); + GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i); NbtMap blockTag = bedrockBlockRuntimeId.getState(); String bedrockName = blockTag.getString("name"); if (!bedrockName.equals(correctBedrockIdentifier)) { @@ -339,6 +359,12 @@ public class ItemRegistryPopulator { // Because we have replaced the Bedrock block ID, we also need to replace the creative contents block runtime ID // That way, creative items work correctly for these blocks + + // Set our custom block override now if there is one + if (customBlockItemOverride != null) { + bedrockBlock = customBlockItemOverride; + } + for (int j = 0; j < creativeItems.size(); j++) { ItemData itemData = creativeItems.get(j); if (itemData.getDefinition().equals(definition)) { @@ -347,16 +373,35 @@ public class ItemRegistryPopulator { } NbtMap states = ((GeyserBedrockBlock) itemData.getBlockDefinition()).getState().getCompound("states"); + boolean valid = true; for (Map.Entry nbtEntry : requiredBlockStates.entrySet()) { - if (!states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) { + if (states.getOrDefault(nbtEntry.getKey(), null) == null || !states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) { // A required block state doesn't match - this one is not valid valid = false; break; } } if (valid) { - creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build()); + if (customBlockItemOverride != null && customBlockData != null) { + // Assuming this is a valid custom block override we'll just register it now while we have the creative item + int customProtocolId = nextFreeBedrockId++; + mappingItem.setBedrockData(customProtocolId); + bedrockIdentifier = customBlockData.identifier(); + definition = new SimpleItemDefinition(bedrockIdentifier, customProtocolId, true); + registry.put(customProtocolId, definition); + customBlockItemDefinitions.put(customBlockData, definition); + customIdMappings.put(customProtocolId, bedrockIdentifier); + + creativeItems.set(j, itemData.toBuilder() + .definition(definition) + .blockDefinition(bedrockBlock) + .netId(itemData.getNetId()) + .count(1) + .build()); + } else { + creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build()); + } break; } } @@ -397,10 +442,10 @@ public class ItemRegistryPopulator { for (CustomItemData customItem : customItemsToLoad) { int customProtocolId = nextFreeBedrockId++; - String customItemName = "geyser_custom:" + customItem.name(); + String customItemName = customItem instanceof NonVanillaCustomItemData nonVanillaItem ? nonVanillaItem.identifier() : Constants.GEYSER_CUSTOM_NAMESPACE + ":" + customItem.name(); if (!registeredItemNames.add(customItemName)) { if (firstMappingsPass) { - GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItem.name() + "' already exists and was registered again! Skipping..."); + GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItemName + "' already exists and was registered again! Skipping..."); } continue; } @@ -516,6 +561,41 @@ public class ItemRegistryPopulator { } } + // Register the item forms of custom blocks + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) { + // We might've registered it already with the vanilla blocks so check first + if (customBlockItemDefinitions.containsKey(customBlock)) { + continue; + } + + // Non-vanilla custom blocks will be handled in the item + // registry, so we don't need to do anything here. + if (customBlock instanceof NonVanillaCustomBlockData) { + continue; + } + + int customProtocolId = nextFreeBedrockId++; + String identifier = customBlock.identifier(); + + final ItemDefinition definition = new SimpleItemDefinition(identifier, customProtocolId, true); + registry.put(customProtocolId, definition); + customBlockItemDefinitions.put(customBlock, definition); + customIdMappings.put(customProtocolId, identifier); + + GeyserBedrockBlock bedrockBlock = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlock.defaultBlockState(), null); + + if (bedrockBlock != null && customBlock.includedInCreativeInventory()) { + creativeItems.add(ItemData.builder() + .definition(definition) + .blockDefinition(bedrockBlock) + .netId(creativeNetId.incrementAndGet()) + .count(1) + .build()); + } + } + } + ItemMappings itemMappings = ItemMappings.builder() .items(mappings.toArray(new ItemMapping[0])) .creativeItems(creativeItems.toArray(new ItemData[0])) @@ -527,6 +607,7 @@ public class ItemRegistryPopulator { .componentItemData(componentItemData) .lodestoneCompass(lodestoneEntry) .customIdMappings(customIdMappings) + .customBlockItemDefinitions(customBlockItemDefinitions) .build(); Registries.ITEMS.register(palette.protocolVersion(), itemMappings); diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java index 34cde0acf..528999158 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java @@ -56,6 +56,7 @@ public class BlockMapping { @Nonnull PistonBehavior pistonBehavior; boolean isBlockEntity; + boolean isNonVanilla; /** * @return the identifier without the additional block states diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java index 992ae324a..3c33ea097 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java @@ -25,12 +25,17 @@ package org.geysermc.geyser.registry.type; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import lombok.Builder; import lombok.Value; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.common.DefinitionRegistry; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import java.util.List; import java.util.Map; import java.util.Set; @@ -44,9 +49,11 @@ public class BlockMappings implements DefinitionRegistry { int blockStateVersion; GeyserBedrockBlock[] javaToBedrockBlocks; + GeyserBedrockBlock[] javaToVanillaBedrockBlocks; Map stateDefinitionMap; GeyserBedrockBlock[] bedrockRuntimeMap; + int[] remappedVanillaIds; BlockDefinition commandBlock; @@ -55,6 +62,10 @@ public class BlockMappings implements DefinitionRegistry { Set jigsawStates; + List blockProperties; + Object2ObjectMap customBlockStateDefinitions; + Int2ObjectMap extendedCollisionBoxes; + public int getBedrockBlockId(int javaState) { return getBedrockBlock(javaState).getRuntimeId(); } @@ -66,6 +77,13 @@ public class BlockMappings implements DefinitionRegistry { return this.javaToBedrockBlocks[javaState]; } + public GeyserBedrockBlock getVanillaBedrockBlock(int javaState) { + if (javaState < 0 || javaState >= this.javaToVanillaBedrockBlocks.length) { + return bedrockAir; + } + return this.javaToVanillaBedrockBlocks[javaState]; + } + public BlockDefinition getItemFrame(NbtMap tag) { return this.itemFrames.get(tag); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java b/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java new file mode 100644 index 000000000..5fe8a0edf --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.type; + +import lombok.Data; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.TransformationComponent; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; +import org.geysermc.geyser.level.block.GeyserCustomBlockData; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +@Data +public class CustomSkull { + private final String skinHash; + + private CustomBlockData customBlockData; + + private static final String BITS_A_PROPERTY = "geyser_skull:bits_a"; + private static final String BITS_B_PROPERTY = "geyser_skull:bits_b"; + + private static final int[] ROTATIONS = {0, -90, 180, 90}; + + private static final BoxComponent FLOOR_BOX = new BoxComponent( + -4, 0, -4, + 8, 8, 8 + ); + + private static final BoxComponent WALL_BOX = new BoxComponent( + -4, 4, 0, + 8, 8, 8 + ); + + public CustomSkull(String skinHash) { + this.skinHash = skinHash; + + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .destructibleByMining(1.5f) + .materialInstance("*", new MaterialInstanceBuilder() + .texture("geyser." + skinHash + "_player_skin") + .renderMethod("alpha_test") + .faceDimming(true) + .ambientOcclusion(true) + .build()) + .lightDampening(0) + .placeAir(true) + .build(); + + List permutations = new ArrayList<>(); + addDefaultPermutation(permutations); + addFloorPermutations(permutations); + addWallPermutations(permutations); + + customBlockData = new GeyserCustomBlockData.CustomBlockDataBuilder() + .name("player_skull_" + skinHash) + .components(components) + .intProperty(BITS_A_PROPERTY, IntStream.rangeClosed(0, 6).boxed().toList()) // This gives us exactly 21 block states + .intProperty(BITS_B_PROPERTY, IntStream.rangeClosed(0, 2).boxed().toList()) + .permutations(permutations) + .build(); + } + + public CustomBlockState getWallBlockState(int wallDirection) { + wallDirection = switch (wallDirection) { + case 0 -> 2; // South + case 90 -> 3; // West + case 180 -> 0; // North + case 270 -> 1; // East + default -> throw new IllegalArgumentException("Unknown skull wall direction: " + wallDirection); + }; + + return customBlockData.blockStateBuilder() + .intProperty(BITS_A_PROPERTY, wallDirection + 1) + .intProperty(BITS_B_PROPERTY, 0) + .build(); + } + + public CustomBlockState getFloorBlockState(int floorRotation) { + return customBlockData.blockStateBuilder() + .intProperty(BITS_A_PROPERTY, (5 + floorRotation) % 7) + .intProperty(BITS_B_PROPERTY, (5 + floorRotation) / 7) + .build(); + } + + private void addDefaultPermutation(List permutations) { + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_hand") + .build()) + .transformation(new TransformationComponent(0, 180, 0)) + .build(); + + String condition = String.format("query.block_property('%s') == 0 && query.block_property('%s') == 0", BITS_A_PROPERTY, BITS_B_PROPERTY); + permutations.add(new CustomBlockPermutation(components, condition)); + } + + private void addFloorPermutations(List permutations) { + String[] quadrantNames = {"a", "b", "c", "d"}; + + for (int quadrant = 0; quadrant < 4; quadrant++) { + for (int i = 0; i < 4; i++) { + int floorRotation = 4 * quadrant + i; + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .selectionBox(FLOOR_BOX) + .collisionBox(FLOOR_BOX) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_floor_" + quadrantNames[i]) + .build()) + .transformation(new TransformationComponent(0, ROTATIONS[quadrant], 0)) + .build(); + + int bitsA = (5 + floorRotation) % 7; + int bitsB = (5 + floorRotation) / 7; + String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, bitsA, BITS_B_PROPERTY, bitsB); + permutations.add(new CustomBlockPermutation(components, condition)); + } + } + } + + private void addWallPermutations(List permutations) { + for (int i = 0; i < 4; i++) { + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .selectionBox(WALL_BOX) + .collisionBox(WALL_BOX) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_wall") + .build()) + .transformation(new TransformationComponent(0, ROTATIONS[i], 0)) + .build(); + + String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, i + 1, BITS_B_PROPERTY, 0); + permutations.add(new CustomBlockPermutation(components, condition)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java index ab8c52bf6..c1ef09b87 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java @@ -30,6 +30,7 @@ import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.ToString; import lombok.With; @@ -39,6 +40,7 @@ import lombok.With; @ToString @EqualsAndHashCode @Getter +@Setter @With @NoArgsConstructor @AllArgsConstructor diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java index 65cc28420..704d5c211 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.registry.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import lombok.Builder; import lombok.Value; import org.checkerframework.checker.nullness.qual.NonNull; @@ -36,6 +37,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.common.DefinitionRegistry; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -72,6 +74,8 @@ public class ItemMappings implements DefinitionRegistry { List componentItemData; Int2ObjectMap customIdMappings; + Object2ObjectMap customBlockItemDefinitions; + /** * Gets an {@link ItemMapping} from the given {@link ItemStack}. * diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index e09cff9e4..7197a4fc4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -464,6 +464,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private long lastInteractionTime; + /** + * Stores when the player started to break a block. Used to allow correct break time for custom blocks. + */ + @Setter + private long blockBreakStartTime; + /** * Stores whether the player intended to place a bucket. */ @@ -1563,6 +1569,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setItemDefinitions(this.itemMappings.getItemDefinitions().values().stream().toList()); // TODO // startGamePacket.setBlockPalette(this.blockMappings.getBedrockBlockPalette()); + // Needed for custom block mappings and custom skulls system + startGamePacket.getBlockProperties().addAll(this.blockMappings.getBlockProperties()); + + // See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment + // data_driven_items (Holiday Creator Features) is needed for blocks and items + startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); + // Needed for block properties for states + startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); + // Needed for certain molang queries used in blocks and items + startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); + startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); startGamePacket.setServerEngine(""); // Do we want to fill this in? diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index ab8528c06..d6e376d8f 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -31,9 +31,17 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockState; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; +import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import java.io.IOException; import java.util.*; public class SkullCache { @@ -71,21 +79,44 @@ public class SkullCache { this.skullRenderDistanceSquared = distance * distance; } - public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { + public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { Skull skull = skulls.computeIfAbsent(position, Skull::new); skull.uuid = uuid; - skull.texturesProperty = texturesProperty; + if (!texturesProperty.equals(skull.texturesProperty)) { + skull.texturesProperty = texturesProperty; + skull.skinHash = null; + try { + SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(texturesProperty); + if (gameProfileData != null && gameProfileData.skinUrl() != null) { + String skinUrl = gameProfileData.skinUrl(); + skull.skinHash = skinUrl.substring(skinUrl.lastIndexOf('/') + 1); + } else { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty); + } + } catch (IOException e) { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + } skull.blockState = blockState; + skull.blockDefinition = translateCustomSkull(skull.skinHash, blockState); + + if (skull.blockDefinition != null) { + reassignSkullEntity(skull); + return skull; + } if (skull.entity != null) { skull.entity.updateSkull(skull); } else { if (!cullingEnabled) { assignSkullEntity(skull); - return; + return skull; } if (lastPlayerPosition == null) { - return; + return skull; } skull.distanceSquared = position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ()); if (skull.distanceSquared < skullRenderDistanceSquared) { @@ -105,24 +136,24 @@ public class SkullCache { } } } + return skull; } public void removeSkull(Vector3i position) { Skull skull = skulls.remove(position); if (skull != null) { - boolean hadEntity = skull.entity != null; - freeSkullEntity(skull); - - if (cullingEnabled) { - inRangeSkulls.remove(skull); - if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) { - // Reassign entity to the closest skull without an entity - assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1)); - } - } + reassignSkullEntity(skull); } } + public Skull updateSkull(Vector3i position, int blockState) { + Skull skull = skulls.get(position); + if (skull != null) { + putSkull(position, skull.uuid, skull.texturesProperty, blockState); + } + return skull; + } + public void updateVisibleSkulls() { if (cullingEnabled) { // No need to recheck skull visibility for small movements @@ -133,6 +164,10 @@ public class SkullCache { inRangeSkulls.clear(); for (Skull skull : skulls.values()) { + if (skull.blockDefinition != null) { + continue; + } + skull.distanceSquared = skull.position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ()); if (skull.distanceSquared > skullRenderDistanceSquared) { freeSkullEntity(skull); @@ -191,6 +226,19 @@ public class SkullCache { } } + private void reassignSkullEntity(Skull skull) { + boolean hadEntity = skull.entity != null; + freeSkullEntity(skull); + + if (cullingEnabled) { + inRangeSkulls.remove(skull); + if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) { + // Reassign entity to the closest skull without an entity + assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1)); + } + } + } + public void clear() { skulls.clear(); inRangeSkulls.clear(); @@ -199,12 +247,33 @@ public class SkullCache { lastPlayerPosition = null; } + private BlockDefinition translateCustomSkull(String skinHash, int blockState) { + CustomSkull customSkull = BlockRegistries.CUSTOM_SKULLS.get(skinHash); + if (customSkull != null) { + byte floorRotation = BlockStateValues.getSkullRotation(blockState); + CustomBlockState customBlockState; + if (floorRotation == -1) { + // Wall skull + int wallDirection = BlockStateValues.getSkullWallDirections().get(blockState); + customBlockState = customSkull.getWallBlockState(wallDirection); + } else { + customBlockState = customSkull.getFloorBlockState(floorRotation); + } + + return session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockState); + } + return null; + } + @RequiredArgsConstructor @Data public static class Skull { private UUID uuid; private String texturesProperty; + private String skinHash; + private int blockState; + private BlockDefinition blockDefinition; private SkullPlayerEntity entity; private final Vector3i position; diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index b88bbe23c..79f181636 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -277,7 +277,7 @@ public class SkinManager { return null; } - static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { + public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index 1037b88ff..41f750990 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -460,7 +460,7 @@ public class SkinProvider { private static Skin supplySkin(UUID uuid, String textureUrl) { try { - byte[] skin = requestImage(textureUrl, null); + byte[] skin = requestImageData(textureUrl, null); return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); } catch (Exception ignored) {} // just ignore I guess @@ -470,7 +470,7 @@ public class SkinProvider { private static Cape supplyCape(String capeUrl, CapeProvider provider) { byte[] cape = EMPTY_CAPE.capeData(); try { - cape = requestImage(capeUrl, provider); + cape = requestImageData(capeUrl, provider); } catch (Exception ignored) { } // just ignore I guess @@ -527,7 +527,7 @@ public class SkinProvider { } @SuppressWarnings("ResultOfMethodCallIgnored") - private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception { + public static BufferedImage requestImage(String imageUrl, CapeProvider provider) throws IOException { BufferedImage image = null; // First see if we have a cached file. We also update the modification stamp so we know when the file was last used @@ -587,6 +587,11 @@ public class SkinProvider { // TODO remove alpha channel } + return image; + } + + private static byte[] requestImageData(String imageUrl, CapeProvider provider) throws Exception { + BufferedImage image = requestImage(imageUrl, provider); byte[] data = bufferedImageToImageData(image); image.flush(); return data; diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java index 0934f33a7..23961694d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java @@ -57,30 +57,32 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { // See BlockInventoryHolder - same concept there except we're also dealing with a specific block state if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); - String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); - if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest")) - && !javaBlockString[1].contains("type=single")) { - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) { + String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); + if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest")) + && !javaBlockString[1].contains("type=single")) { + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); - NbtMapBuilder tag = NbtMap.builder() - .putString("id", "Chest") - .putInt("x", session.getLastInteractionBlockPosition().getX()) - .putInt("y", session.getLastInteractionBlockPosition().getY()) - .putInt("z", session.getLastInteractionBlockPosition().getZ()) - .putString("CustomName", inventory.getTitle()) - .putString("id", "Chest"); + NbtMapBuilder tag = NbtMap.builder() + .putString("id", "Chest") + .putInt("x", session.getLastInteractionBlockPosition().getX()) + .putInt("y", session.getLastInteractionBlockPosition().getY()) + .putInt("z", session.getLastInteractionBlockPosition().getZ()) + .putString("CustomName", inventory.getTitle()) + .putString("id", "Chest"); - DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId); - DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue, - session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); + DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId); + DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue, + session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); - BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); - dataPacket.setData(tag.build()); - dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); - session.sendUpstreamPacket(dataPacket); + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); + dataPacket.setData(tag.build()); + dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); + session.sendUpstreamPacket(dataPacket); - return true; + return true; + } } } @@ -90,7 +92,7 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { } Vector3i pairPosition = position.add(Vector3i.UNIT_X); - BlockDefinition definition = session.getBlockMappings().getBedrockBlock(defaultJavaBlockState); + BlockDefinition definition = session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java index 0f0421be8..336809e8a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java @@ -28,9 +28,23 @@ package org.geysermc.geyser.translator.inventory.item; import com.github.steveice10.mc.protocol.data.game.Identifier; import com.github.steveice10.mc.protocol.data.game.entity.attribute.ModifierOperation; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.opennbt.tag.builtin.*; +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.FloatTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; +import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtList; @@ -41,12 +55,15 @@ import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -143,9 +160,21 @@ public final class ItemTranslator { ItemData.Builder builder = javaItem.translateToBedrock(itemStack, bedrockItem, session.getItemMappings()); if (bedrockItem.isBlock()) { - builder.blockDefinition(bedrockItem.getBedrockBlockDefinition()); + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault( + bedrockItem.getJavaItem().javaIdentifier(), null); + if (customBlockData != null) { + translateCustomBlock(customBlockData, session, builder); + } else { + builder.blockDefinition(bedrockItem.getBedrockBlockDefinition()); + } } + if (bedrockItem.getJavaItem().equals(Items.PLAYER_HEAD)) { + translatePlayerHead(session, nbt, builder); + } + + translateCustomItem(nbt, builder, bedrockItem); + if (nbt != null) { // Translate the canDestroy and canPlaceOn Java NBT ListTag canDestroy = nbt.get("CanDestroy"); @@ -362,10 +391,24 @@ public final class ItemTranslator { ItemMapping mapping = itemStack.asItem().toBedrockDefinition(itemStack.getNbt(), session.getItemMappings()); + ItemDefinition itemDefinition = mapping.getBedrockDefinition(); + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault( + mapping.getJavaItem().javaIdentifier(), null); + if (customBlockData != null) { + itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + } + + if (mapping.getJavaItem().equals(Items.PLAYER_HEAD)) { + CustomSkull customSkull = getCustomSkull(session, itemStack.getNbt()); + if (customSkull != null) { + itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customSkull.getCustomBlockData()); + } + } + ItemDefinition definition = CustomItemTranslator.getCustomItem(itemStack.getNbt(), mapping); if (definition == null) { // No custom item - return mapping.getBedrockDefinition(); + return itemDefinition; } else { return definition; } @@ -553,6 +596,46 @@ public final class ItemTranslator { ItemDefinition definition = CustomItemTranslator.getCustomItem(nbt, mapping); if (definition != null) { builder.definition(definition); + builder.blockDefinition(null); + } + } + + /** + * Translates a custom block override + */ + private static void translateCustomBlock(CustomBlockData customBlockData, GeyserSession session, ItemData.Builder builder) { + ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState()); + builder.definition(itemDefinition); + builder.blockDefinition(blockDefinition); + } + + private static CustomSkull getCustomSkull(GeyserSession session, CompoundTag nbt) { + if (nbt != null && nbt.contains("SkullOwner")) { + if (!(nbt.get("SkullOwner") instanceof CompoundTag skullOwner)) { + // It's a username give up d: + return null; + } + SkinManager.GameProfileData data = SkinManager.GameProfileData.from(skullOwner); + if (data == null) { + session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + nbt); + return null; + } + + String skinHash = data.skinUrl().substring(data.skinUrl().lastIndexOf('/') + 1); + return BlockRegistries.CUSTOM_SKULLS.get(skinHash); + } + return null; + } + + private static void translatePlayerHead(GeyserSession session, CompoundTag nbt, ItemData.Builder builder) { + CustomSkull customSkull = getCustomSkull(session, nbt); + if (customSkull != null) { + CustomBlockData customBlockData = customSkull.getCustomBlockData(); + ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState()); + builder.definition(itemDefinition); + builder.blockDefinition(blockDefinition); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java index cab42f3d0..634b7c6f1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java @@ -42,7 +42,7 @@ import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.level.physics.Direction; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.collision.BlockCollision; @@ -95,7 +95,7 @@ public class PistonBlockEntity { static { // Create a ~1 x ~0.5 x ~1 bounding box above the honey block - BlockCollision blockCollision = Registries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID); + BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID); if (blockCollision == null) { throw new RuntimeException("Failed to find honey block collision"); } @@ -485,7 +485,7 @@ public class PistonBlockEntity { pistonCache.displacePlayer(movement.mul(delta)); } else { // Move the player out of collision - BlockCollision blockCollision = Registries.COLLISIONS.get(javaId); + BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(javaId); if (blockCollision != null) { Vector3d extend = movement.mul(Math.min(1 - blockMovement, 0.5)); Direction movementDirection = orientation; diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index bc624ed4e..ace4d77b8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -30,10 +30,14 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; +import org.geysermc.geyser.GeyserImpl; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMapBuilder; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkinProvider; import java.nio.charset.StandardCharsets; @@ -41,6 +45,7 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; @BlockEntity(type = BlockEntityType.SKULL) public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { @@ -91,21 +96,55 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements return CompletableFuture.completedFuture(texture.getValue()); } - public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) { - Vector3i blockPosition = Vector3i.from(posX, posY, posZ); + public static BlockDefinition translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) { CompoundTag owner = tag.get("SkullOwner"); if (owner == null) { session.getSkullCache().removeSkull(blockPosition); - return; + return null; + } + UUID uuid = getUUID(owner); + + CompletableFuture texturesFuture = getTextures(owner, uuid); + if (texturesFuture.isDone()) { + try { + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesFuture.get(), blockState); + return skull.getBlockDefinition(); + } catch (InterruptedException | ExecutionException e) { + session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + return null; } - UUID uuid = getUUID(owner); - getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> { + // SkullOwner contained a username, so we have to wait for it to be retrieved + texturesFuture.whenComplete((texturesProperty, throwable) -> { + if (texturesProperty == null) { + session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag); + return; + } if (session.getEventLoop().inEventLoop()) { - session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); + putSkull(session, blockPosition, uuid, texturesProperty, blockState); } else { - session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState)); + session.executeInEventLoop(() -> putSkull(session, blockPosition, uuid, texturesProperty, blockState)); } }); + + // We don't have the textures yet, so we can't determine if a custom block was defined for this skull + return null; + } + + private static void putSkull(GeyserSession session, Vector3i blockPosition, UUID uuid, String texturesProperty, int blockState) { + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); + if (skull.getBlockDefinition() != null) { + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(blockPosition); + updateBlockPacket.setDefinition(skull.getBlockDefinition()); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); + session.sendUpstreamPacket(updateBlockPacket); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index bf2c8b1cc..a614663ed 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -39,6 +39,7 @@ import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -66,6 +67,7 @@ import org.geysermc.geyser.item.type.SpawnEggItem; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; @@ -181,6 +183,27 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator belowBlockPos = blockPos.add(0, -2, 0); + case 2 -> belowBlockPos = blockPos.add(0, -1, 1); + case 3 -> belowBlockPos = blockPos.add(0, -1, -1); + case 4 -> belowBlockPos = blockPos.add(1, -1, 0); + case 5 -> belowBlockPos = blockPos.add(-1, -1, 0); + } + + if (belowBlockPos != null) { + int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, belowBlockPos); + BlockDefinition extendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock); + if (extendedCollisionDefinition != null && (System.currentTimeMillis() - session.getLastInteractionTime()) < 200) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + } + } + // Check to make sure the client isn't spamming interaction // Based on Nukkit 1.0, with changes to ensure holding down still works boolean hasAlreadyClicked = System.currentTimeMillis() - session.getLastInteractionTime() < 110.0 && @@ -265,6 +288,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator= (breakTime+=2) * 50) { + // Play break sound and particle + LevelEventPacket effectPacket = new LevelEventPacket(); + effectPacket.setPosition(vectorFloat); + effectPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); + effectPacket.setData(session.getBlockMappings().getBedrockBlockId(breakingBlock)); + session.sendUpstreamPacket(effectPacket); + + // Break the block + ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING, + vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); + session.sendDownstreamPacket(finishBreakingPacket); + session.setBlockBreakStartTime(0); + break; + } + } + updateBreak.setData((int) (65535 / breakTime)); session.sendUpstreamPacket(updateBreak); break; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 1af0ff814..7a14cebcb 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -110,7 +110,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData(); ItemData output = ItemTranslator.translateToBedrock(session, shapelessRecipeData.getResult()); - if (output.equals(ItemData.AIR)) { + if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists continue; } @@ -131,7 +131,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); ItemData output = ItemTranslator.translateToBedrock(session, shapedRecipeData.getResult()); - if (output.equals(ItemData.AIR)) { + if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists continue; } @@ -213,7 +213,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { + private static final ThreadLocal EXTENDED_COLLISIONS_STORAGE = ThreadLocal.withInitial(ExtendedCollisionsStorage::new); @Override public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) { + final boolean useExtendedCollisions = !session.getBlockMappings().getExtendedCollisionBoxes().isEmpty(); + if (session.isSpawned()) { ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt()); } @@ -111,19 +118,47 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4)); if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client + if (useExtendedCollisions) { + EXTENDED_COLLISIONS_STORAGE.get().clear(); + } + extendedCollisionNextSection = false; continue; } // No need to encode an empty section... if (javaSection.isBlockCountEmpty()) { + // Unless we need to send extended collisions + if (useExtendedCollisions) { + if (extendedCollision) { + int blocks = EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() + 1; + BitArray bedrockData = BitArrayVersion.forBitsCeil(Integer.SIZE - Integer.numberOfLeadingZeros(blocks)).createArray(BlockStorage.SIZE); + BlockStorage layer0 = new BlockStorage(bedrockData, new IntArrayList(blocks)); + + layer0.idFor(session.getBlockMappings().getBedrockAir().getRuntimeId()); + for (int yzx = 0; yzx < BlockStorage.SIZE / 16; yzx++) { + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + bedrockData.set(indexYZXtoXZY(yzx), layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + } + } + + BlockStorage[] layers = new BlockStorage[]{ layer0 }; + sections[bedrockSectionY] = new GeyserChunkSection(layers, bedrockSectionY); + } + EXTENDED_COLLISIONS_STORAGE.get().clear(); + extendedCollisionNextSection = false; + } continue; } @@ -143,6 +178,24 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 5]; @@ -231,6 +311,64 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 5] |= 1 << (xzy & 0x1F); } } + + // V1 palette + IntList layer1Palette = IntList.of( + session.getBlockMappings().getBedrockAir().getRuntimeId(), // Air - see BlockStorage's constructor for more information + session.getBlockMappings().getBedrockWater().getRuntimeId()); + + layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; + } else if (waterloggedPaletteIds.isEmpty() && extendedCollision) { + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + int paletteId = javaData.get(yzx); + int xzy = indexYZXtoXZY(yzx); + bedrockData.set(xzy, paletteId); + + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + if (paletteId == airPaletteId) { + bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + } + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + continue; + } + BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings() + .getExtendedCollisionBoxes().get(javaPalette.idToState(paletteId)); + if (aboveBedrockExtendedCollisionDefinition != null) { + EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY); + if ((xzy & 0xF) == 15) { + thisExtendedCollisionNextSection = true; + } + } + } + + layers = new BlockStorage[]{ layer0 }; + } else { + int[] layer1Data = new int[BlockStorage.SIZE >> 5]; + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + int paletteId = javaData.get(yzx); + int xzy = indexYZXtoXZY(yzx); + bedrockData.set(xzy, paletteId); + + if (waterloggedPaletteIds.get(paletteId)) { + layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F); + } + + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + if (paletteId == airPaletteId) { + bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + } + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + continue; + } + BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes() + .get(javaPalette.idToState(paletteId)); + if (aboveBedrockExtendedCollisionDefinition != null) { + EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY); + if ((xzy & 0xF) == 15) { + thisExtendedCollisionNextSection = true; + } + } + } // V1 palette IntList layer1Palette = IntList.of( @@ -241,6 +379,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4) - (bedrockDimension.minY() >> 4); + if (0 <= bedrockSectionY && bedrockSectionY < maxBedrockSectionY) { + // Custom skull is in a section accepted by Bedrock + GeyserChunkSection bedrockSection = sections[bedrockSectionY]; + IntList palette = bedrockSection.getBlockStorageArray()[0].getPalette(); + if (palette instanceof IntImmutableList || palette instanceof IntLists.Singleton) { + // TODO there has to be a better way to expand the palette .-. + bedrockSection = bedrockSection.copy(bedrockSectionY); + sections[bedrockSectionY] = bedrockSection; + } + bedrockSection.setFullBlock(x, y & 0xF, z, 0, blockDefinition.getRuntimeId()); + } + } } } @@ -380,4 +533,50 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator