From 36c49a7256ae9a08aed82f7421c355b2d806a550 Mon Sep 17 00:00:00 2001 From: ImDaBigBoss <67973871+ImDaBigBoss@users.noreply.github.com> Date: Sat, 2 Jul 2022 18:50:16 +0200 Subject: [PATCH] Custom item support for extensions (#2822) Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- .gitignore | 3 +- .../org/geysermc/geyser/api/GeyserApi.java | 2 +- .../geysermc/geyser/api/event/EventBus.java | 2 +- .../GeyserDefineCustomItemsEvent.java | 85 +++++ .../GeyserLoadResourcePacksEvent.java | 40 ++ .../api/item/custom/CustomItemData.java | 109 ++++++ .../api/item/custom/CustomItemOptions.java | 83 ++++ .../api/item/custom/CustomRenderOffsets.java | 51 +++ .../item/custom/NonVanillaCustomItemData.java | 188 +++++++++ .../geysermc/geyser/api/util/TriState.java | 83 ++++ bootstrap/standalone/build.gradle.kts | 2 +- .../java/org/geysermc/geyser/GeyserImpl.java | 28 +- .../geyser/entity/type/ItemFrameEntity.java | 7 +- .../extension/GeyserExtensionManager.java | 24 +- .../geyser/item/GeyserCustomItemData.java | 158 ++++++++ .../geyser/item/GeyserCustomItemOptions.java | 69 ++++ .../geyser/item/GeyserCustomMappingData.java | 32 ++ .../item/GeyserNonVanillaCustomItemData.java | 303 +++++++++++++++ .../item/components/ToolBreakSpeedsUtils.java | 174 +++++++++ .../geyser/item/components/ToolTier.java | 66 ++++ .../geyser/item/components/WearableSlot.java | 47 +++ .../InvalidCustomMappingsFileException.java | 40 ++ .../item/mappings/MappingsConfigReader.java | 98 +++++ .../mappings/versions/MappingsReader.java | 93 +++++ .../mappings/versions/MappingsReader_v1.java | 123 ++++++ .../geyser/network/UpstreamPacketHandler.java | 4 +- .../geysermc/geyser/pack/ResourcePack.java | 35 +- .../geysermc/geyser/registry/Registries.java | 5 - .../CustomItemRegistryPopulator.java | 360 ++++++++++++++++++ .../populator/ItemRegistryPopulator.java | 159 +++++++- .../provider/GeyserBuilderProvider.java | 9 + .../registry/type/GeyserMappingItem.java | 2 + .../geyser/registry/type/ItemMapping.java | 9 +- .../geyser/registry/type/ItemMappings.java | 5 +- .../type/NonVanillaItemRegistration.java | 34 ++ .../geyser/session/GeyserSession.java | 6 +- .../inventory/item/CompassTranslator.java | 3 +- .../inventory/item/ItemTranslator.java | 55 ++- .../inventory/item/PotionTranslator.java | 3 +- .../inventory/item/TippedArrowTranslator.java | 3 +- .../inventory/item/nbt/BannerTranslator.java | 3 +- .../JavaMerchantOffersTranslator.java | 3 +- core/src/main/resources/mappings | 2 +- 43 files changed, 2536 insertions(+), 74 deletions(-) create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomRenderOffsets.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java create mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/util/TriState.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/components/WearableSlot.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/exception/InvalidCustomMappingsFileException.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java create 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/registry/populator/CustomItemRegistryPopulator.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/type/NonVanillaItemRegistration.java diff --git a/.gitignore b/.gitignore index 0566cb462..2b7e2972c 100644 --- a/.gitignore +++ b/.gitignore @@ -249,4 +249,5 @@ locales/ /packs/ /dump.json /saved-refresh-tokens.json -/languages/ \ No newline at end of file +/custom_mappings/ +/languages/ diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java index 96c23d267..d6cb3c25a 100644 --- a/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java @@ -116,7 +116,7 @@ public interface GeyserApi extends GeyserApiBase { EventBus eventBus(); /** - * Get's the default {@link RemoteServer} configured + * Gets the default {@link RemoteServer} configured * within the config file that is used by default. * * @return the default remote server used within Geyser diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/EventBus.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/EventBus.java index 0352dcc9e..b13f12300 100644 --- a/api/geyser/src/main/java/org/geysermc/geyser/api/event/EventBus.java +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/EventBus.java @@ -76,7 +76,7 @@ public interface EventBus { void unregisterAll(@NonNull Extension extension); /** - * Fires the given {@link Event}. + * Fires the given {@link Event} and returns the result. * * @param event the event to fire * diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java new file mode 100644 index 000000000..308b39d22 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java @@ -0,0 +1,85 @@ +/* + * 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 com.google.common.collect.Multimap; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.Event; +import org.geysermc.geyser.api.item.custom.CustomItemData; +import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; + +import java.util.*; + +/** + * Called on Geyser's startup when looking for custom items. Custom items 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 GeyserDefineCustomItemsEvent implements Event { + private final Multimap customItems; + private final List nonVanillaCustomItems; + + public GeyserDefineCustomItemsEvent(Multimap customItems, List nonVanillaCustomItems) { + this.customItems = customItems; + this.nonVanillaCustomItems = nonVanillaCustomItems; + } + + /** + * Gets a multimap of all the already registered custom items indexed by the item's extended java item's identifier. + * + * @return a multimap of all the already registered custom items + */ + public Map> getExistingCustomItems() { + return Collections.unmodifiableMap(this.customItems.asMap()); + } + + /** + * Gets the list of the already registered non-vanilla custom items. + * + * @return the list of the already registered non-vanilla custom items + */ + public List getExistingNonVanillaCustomItems() { + return Collections.unmodifiableList(this.nonVanillaCustomItems); + } + + /** + * Registers a custom item with a base Java item. This is used to register items with custom textures and properties + * based on NBT data. + * + * @param identifier the base (java) item + * @param customItemData the custom item data to register + * @return if the item was registered + */ + public abstract boolean register(@NonNull String identifier, @NonNull CustomItemData customItemData); + + /** + * Registers a custom item with no base item. This is used for mods. + * + * @param customItemData the custom item data to register + * @return if the item was registered + */ + public abstract boolean register(@NonNull NonVanillaCustomItemData customItemData); +} \ No newline at end of file diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java new file mode 100644 index 000000000..0f181aedf --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.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.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.event.Event; + +import java.nio.file.Path; +import java.util.List; + +/** + * Called when resource packs are loaded within Geyser. + * + * @param resourcePacks a mutable list of the currently listed resource packs + */ +public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java new file mode 100644 index 000000000..7391c0680 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java @@ -0,0 +1,109 @@ +/* + * 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.item.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +/** + * This is used to store data for a custom item. + */ +public interface CustomItemData { + /** + * Gets the item's name. + * + * @return the item's name + */ + @NonNull String name(); + + /** + * Gets the custom item options of the item. + * + * @return the custom item options of the item. + */ + CustomItemOptions customItemOptions(); + + /** + * Gets the item's display name. By default, this is the item's name. + * + * @return the item's display name + */ + @NonNull String displayName(); + + /** + * Gets the item's icon. By default, this is the item's name. + * + * @return the item's icon + */ + @NonNull String icon(); + + /** + * Gets if the item is allowed to be put into the offhand. + * + * @return true if the item is allowed to be used in the offhand, false otherwise + */ + boolean allowOffhand(); + + /** + * Gets the item's texture size. This is to resize the item if the texture is not 16x16. + * + * @return the item's texture size + */ + int textureSize(); + + /** + * Gets the item's render offsets. If it is null, the item will be rendered normally, with no offsets. + * + * @return the item's render offsets + */ + @Nullable CustomRenderOffsets renderOffsets(); + + static CustomItemData.Builder builder() { + return GeyserApi.api().providerManager().builderProvider().provideBuilder(CustomItemData.Builder.class); + } + + interface Builder { + /** + * Will also set the display name and icon to the provided parameter, if it is currently not set. + */ + Builder name(@NonNull String name); + + Builder customItemOptions(@NonNull CustomItemOptions customItemOptions); + + Builder displayName(@NonNull String displayName); + + Builder icon(@NonNull String icon); + + Builder allowOffhand(boolean allowOffhand); + + Builder textureSize(int textureSize); + + Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets); + + CustomItemData build(); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java new file mode 100644 index 000000000..037f2f05e --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java @@ -0,0 +1,83 @@ +/* + * 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.item.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.TriState; + +import java.util.OptionalInt; + +/** + * This class represents the different ways you can register custom items + */ +public interface CustomItemOptions { + /** + * Gets if the item should be unbreakable. + * + * @return if the item should be unbreakable + */ + @NonNull TriState unbreakable(); + + /** + * Gets the item's custom model data predicate. + * + * @return the item's custom model data + */ + @NonNull OptionalInt customModelData(); + + /** + * Gets the item's damage predicate. + * + * @return the item's damage predicate + */ + @NonNull OptionalInt damagePredicate(); + + /** + * Checks if the item has at least one option set + * + * @return true if the item at least one options set + */ + default boolean hasCustomItemOptions() { + return this.unbreakable() != TriState.NOT_SET || + this.customModelData().isPresent() || + this.damagePredicate().isPresent(); + } + + static CustomItemOptions.Builder builder() { + return GeyserApi.api().providerManager().builderProvider().provideBuilder(CustomItemOptions.Builder.class); + } + + interface Builder { + Builder unbreakable(boolean unbreakable); + + Builder customModelData(int customModelData); + + Builder damagePredicate(int damagePredicate); + + CustomItemOptions build(); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomRenderOffsets.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomRenderOffsets.java new file mode 100644 index 000000000..f81da0ae2 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomRenderOffsets.java @@ -0,0 +1,51 @@ +/* + * 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.item.custom; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This class is used to store the render offsets of custom items. + */ +public record CustomRenderOffsets(@Nullable Hand mainHand, @Nullable Hand offhand) { + /** + * The hand that is used for the offset. + */ + public record Hand(@Nullable Offset firstPerson, @Nullable Offset thirdPerson) { + } + + /** + * The offset of the item. + */ + public record Offset(@Nullable OffsetXYZ position, @Nullable OffsetXYZ rotation, @Nullable OffsetXYZ scale) { + } + + /** + * X, Y and Z positions for the offset. + */ + public record OffsetXYZ(float x, float y, float z) { + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java new file mode 100644 index 000000000..1df94f7ea --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java @@ -0,0 +1,188 @@ +/* + * 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.item.custom; + +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; + +import java.util.OptionalInt; +import java.util.Set; + +/** + * Represents a completely custom item that is not based on an existing vanilla Minecraft item. + */ +public interface NonVanillaCustomItemData extends CustomItemData { + /** + * Gets the java identifier for this item. + * + * @return The java identifier for this item. + */ + @NonNull String identifier(); + + /** + * Gets the java item id of the item. + * + * @return the java item id of the item + */ + @NonNegative int javaId(); + + /** + * Gets the stack size of the item. + * + * @return the stack size of the item + */ + @NonNegative int stackSize(); + + /** + * Gets the max damage of the item. + * + * @return the max damage of the item + */ + int maxDamage(); + + /** + * Gets the tool type of the item. + * + * @return the tool type of the item + */ + @Nullable String toolType(); + + /** + * Gets the tool tier of the item. + * + * @return the tool tier of the item + */ + @Nullable String toolTier(); + + /** + * Gets the armor type of the item. + * + * @return the armor type of the item + */ + @Nullable String armorType(); + + /** + * Gets the armor protection value of the item. + * + * @return the armor protection value of the item + */ + int protectionValue(); + + /** + * Gets the item's translation string. + * + * @return the item's translation string + */ + @Nullable String translationString(); + + /** + * Gets the repair materials of the item. + * + * @return the repair materials of the item + */ + @Nullable Set repairMaterials(); + + /** + * Gets the item's creative category, or tab id. + * + * @return the item's creative category + */ + @NonNull OptionalInt creativeCategory(); + + /** + * Gets the item's creative group. + * + * @return the item's creative group + */ + @Nullable String creativeGroup(); + + /** + * Gets if the item is a hat. This is used to determine if the item should be rendered on the player's head, and + * normally allow the player to equip it. This is not meant for armor. + * + * @return if the item is a hat + */ + boolean isHat(); + + /** + * Gets if the item is a tool. This is used to set the render type of the item, if the item is handheld. + * + * @return if the item is a tool + */ + boolean isTool(); + + static NonVanillaCustomItemData.Builder builder() { + return GeyserApi.api().providerManager().builderProvider().provideBuilder(NonVanillaCustomItemData.Builder.class); + } + + interface Builder extends CustomItemData.Builder { + Builder name(@NonNull String name); + + Builder identifier(@NonNull String identifier); + + Builder javaId(@NonNegative int javaId); + + Builder stackSize(@NonNegative int stackSize); + + Builder maxDamage(int maxDamage); + + Builder toolType(@Nullable String toolType); + + Builder toolTier(@Nullable String toolTier); + + Builder armorType(@Nullable String armorType); + + Builder protectionValue(int protectionValue); + + Builder translationString(@Nullable String translationString); + + Builder repairMaterials(@Nullable Set repairMaterials); + + Builder creativeCategory(int creativeCategory); + + Builder creativeGroup(@Nullable String creativeGroup); + + Builder hat(boolean isHat); + + Builder tool(boolean isTool); + + @Override + Builder displayName(@NonNull String displayName); + + @Override + Builder allowOffhand(boolean allowOffhand); + + @Override + Builder textureSize(int textureSize); + + @Override + Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets); + + NonVanillaCustomItemData build(); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/util/TriState.java b/api/geyser/src/main/java/org/geysermc/geyser/api/util/TriState.java new file mode 100644 index 000000000..457a38e32 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/util/TriState.java @@ -0,0 +1,83 @@ +/* + * 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.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This is a way to represent a boolean, but with a non set value added. + * This class was inspired by adventure's version https://github.com/KyoriPowered/adventure/blob/main/4/api/src/main/java/net/kyori/adventure/util/TriState.java + */ +public enum TriState { + /** + * Describes a value that is not set, null, or not present. + */ + NOT_SET, + + /** + * Describes a true value. + */ + TRUE, + + /** + * Describes a false value. + */ + FALSE; + + /** + * Converts the TriState to a boolean. + * + * @return the boolean value of the TriState + */ + public @Nullable Boolean toBoolean() { + return switch (this) { + case TRUE -> true; + case FALSE -> false; + default -> null; + }; + } + + /** + * Creates a TriState from a boolean. + * + * @param value the Boolean value + * @return the created TriState + */ + public static @NonNull TriState fromBoolean(@Nullable Boolean value) { + return value == null ? NOT_SET : fromBoolean(value.booleanValue()); + } + + /** + * Creates a TriState from a primitive boolean. + * + * @param value the boolean value + * @return the created TriState + */ + public @NonNull static TriState fromBoolean(boolean value) { + return value ? TRUE : FALSE; + } +} diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index 088d5dc82..d49c7c490 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -1,7 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer val terminalConsoleVersion = "1.2.0" -val jlineVersion = "3.10.0" +val jlineVersion = "3.21.0" dependencies { api(projects.core) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 9eeae4abb..8f3156abe 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -180,22 +180,26 @@ public class GeyserImpl implements GeyserApi { logger.info(""); logger.info("******************************************"); - /* Initialize translators and registries */ - BlockRegistries.init(); - Registries.init(); + /* Initialize event bus */ + this.eventBus = new GeyserEventBus(); + /* Load Extensions */ + this.extensionManager = new GeyserExtensionManager(); + this.extensionManager.init(); + + this.extensionManager.enableExtensions(); + this.eventBus.fire(new GeyserPreInitializeEvent(this.extensionManager, this.eventBus)); + + /* Initialize registries */ + Registries.init(); + BlockRegistries.init(); + + /* Initialize translators */ EntityDefinitions.init(); ItemTranslator.init(); MessageTranslator.init(); MinecraftLocale.init(); - /* Load Extensions */ - this.eventBus = new GeyserEventBus(); - this.extensionManager = new GeyserExtensionManager(); - this.extensionManager.init(); - - this.eventBus.fire(new GeyserPreInitializeEvent(this.extensionManager, this.eventBus)); - start(); GeyserConfiguration config = bootstrap.getGeyserConfig(); @@ -256,8 +260,6 @@ public class GeyserImpl implements GeyserApi { ResourcePack.loadPacks(); - this.extensionManager.enableExtensions(); - if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) { // Set the remote address to localhost since that is where we are always connecting try { @@ -580,6 +582,7 @@ public class GeyserImpl implements GeyserApi { @Override public void reload() { shutdown(); + this.extensionManager.enableExtensions(); bootstrap.onEnable(); } @@ -615,7 +618,6 @@ public class GeyserImpl implements GeyserApi { return this.eventBus; } - @Override public RemoteServer defaultRemoteServer() { return this.remoteServer; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java index bc7736e9b..8e4a5323a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java @@ -40,7 +40,6 @@ import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.util.InteractionResult; @@ -114,7 +113,9 @@ public class ItemFrameEntity extends Entity { if (entityMetadata.getValue() != null) { this.heldItem = entityMetadata.getValue(); ItemData itemData = ItemTranslator.translateToBedrock(session, heldItem); - ItemMapping mapping = session.getItemMappings().getMapping(entityMetadata.getValue()); + + String customIdentifier = session.getItemMappings().getCustomIdMappings().get(itemData.getId()); + NbtMapBuilder builder = NbtMap.builder(); builder.putByte("Count", (byte) itemData.getCount()); @@ -122,7 +123,7 @@ public class ItemFrameEntity extends Entity { builder.put("tag", itemData.getTag()); } builder.putShort("Damage", (short) itemData.getDamage()); - builder.putString("Name", mapping.getBedrockIdentifier()); + builder.putString("Name", customIdentifier != null ? customIdentifier : session.getItemMappings().getMapping(entityMetadata.getValue()).getBedrockIdentifier()); NbtMapBuilder tag = getDefaultTag().toBuilder(); tag.put("Item", builder.build()); tag.putFloat("ItemDropChance", 1.0f); diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java index 79cf1febc..7d80c2cf6 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.extension; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -32,7 +33,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.extension.ExtensionLoader; import org.geysermc.geyser.api.extension.ExtensionManager; -import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.text.GeyserLocale; import java.util.Collection; @@ -44,13 +44,15 @@ import java.util.stream.Collectors; public class GeyserExtensionManager extends ExtensionManager { private static final Key BASE_EXTENSION_LOADER_KEY = Key.key("geysermc", "base"); + private final Map extensionLoaderTypes = new Object2ObjectOpenHashMap<>(); + private final Map extensions = new LinkedHashMap<>(); private final Map extensionsLoaders = new LinkedHashMap<>(); public void init() { GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.loading")); - Registries.EXTENSION_LOADERS.register(BASE_EXTENSION_LOADER_KEY, new GeyserExtensionLoader()); + extensionLoaderTypes.put(BASE_EXTENSION_LOADER_KEY, new GeyserExtensionLoader()); for (ExtensionLoader loader : this.extensionLoaders().values()) { this.loadAllExtensions(loader); } @@ -98,6 +100,12 @@ public class GeyserExtensionManager extends ExtensionManager { } } + public void enableExtensions() { + for (Extension extension : this.extensions()) { + this.enable(extension); + } + } + private void disableExtension(@NonNull Extension extension) { if (extension.isEnabled()) { GeyserImpl.getInstance().eventBus().unregisterAll(extension); @@ -107,12 +115,6 @@ public class GeyserExtensionManager extends ExtensionManager { } } - public void enableExtensions() { - for (Extension extension : this.extensions()) { - this.enable(extension); - } - } - public void disableExtensions() { for (Extension extension : this.extensions()) { this.disable(extension); @@ -133,18 +135,18 @@ public class GeyserExtensionManager extends ExtensionManager { @Nullable @Override public ExtensionLoader extensionLoader(@NonNull String identifier) { - return Registries.EXTENSION_LOADERS.get(Key.key(identifier)); + return this.extensionLoaderTypes.get(Key.key(identifier)); } @Override public void registerExtensionLoader(@NonNull String identifier, @NonNull ExtensionLoader extensionLoader) { - Registries.EXTENSION_LOADERS.register(Key.key(identifier), extensionLoader); + this.extensionLoaderTypes.put(Key.key(identifier), extensionLoader); } @NonNull @Override public Map extensionLoaders() { - return Registries.EXTENSION_LOADERS.get().entrySet().stream().collect(Collectors.toMap(key -> key.getKey().asString(), Map.Entry::getValue)); + return this.extensionLoaderTypes.entrySet().stream().collect(Collectors.toMap(key -> key.getKey().asString(), Map.Entry::getValue)); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java new file mode 100644 index 000000000..ddea9937c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java @@ -0,0 +1,158 @@ +/* + * 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; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.CustomItemData; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; +import org.jetbrains.annotations.NotNull; + +@EqualsAndHashCode +@ToString +public class GeyserCustomItemData implements CustomItemData { + private final String name; + private final CustomItemOptions customItemOptions; + private final String displayName; + private final String icon; + private final boolean allowOffhand; + private final int textureSize; + private final CustomRenderOffsets renderOffsets; + + public GeyserCustomItemData(String name, + CustomItemOptions customItemOptions, + String displayName, + String icon, + boolean allowOffhand, + int textureSize, + CustomRenderOffsets renderOffsets) { + this.name = name; + this.customItemOptions = customItemOptions; + this.displayName = displayName; + this.icon = icon; + this.allowOffhand = allowOffhand; + this.textureSize = textureSize; + this.renderOffsets = renderOffsets; + } + + public @NotNull String name() { + return name; + } + + public CustomItemOptions customItemOptions() { + return customItemOptions; + } + + public @NotNull String displayName() { + return displayName; + } + + public @NotNull String icon() { + return icon; + } + + public boolean allowOffhand() { + return allowOffhand; + } + + public int textureSize() { + return textureSize; + } + + public CustomRenderOffsets renderOffsets() { + return renderOffsets; + } + + public static class CustomItemDataBuilder implements Builder { + protected String name = null; + protected CustomItemOptions customItemOptions = null; + + protected String displayName = null; + protected String icon = null; + protected boolean allowOffhand = true; // Bedrock doesn't give items offhand allowance unless they serve gameplay purpose, but we want to be friendly with Java + protected int textureSize = 16; + protected CustomRenderOffsets renderOffsets = null; + + @Override + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + @Override + public Builder customItemOptions(@NonNull CustomItemOptions customItemOptions) { + this.customItemOptions = customItemOptions; + return this; + } + + @Override + public Builder displayName(@NonNull String displayName) { + this.displayName = displayName; + return this; + } + + @Override + public Builder icon(@NonNull String icon) { + this.icon = icon; + return this; + } + + @Override + public Builder allowOffhand(boolean allowOffhand) { + this.allowOffhand = allowOffhand; + return this; + } + + @Override + public Builder textureSize(int textureSize) { + this.textureSize = textureSize; + return this; + } + + @Override + public Builder renderOffsets(CustomRenderOffsets renderOffsets) { + this.renderOffsets = renderOffsets; + return this; + } + + @Override + public CustomItemData build() { + if (this.name == null || this.customItemOptions == null) { + throw new IllegalArgumentException("Name and custom item options must be set"); + } + + if (this.displayName == null) { + this.displayName = this.name; + } + if (this.icon == null) { + this.icon = this.name; + } + return new GeyserCustomItemData(this.name, this.customItemOptions, this.displayName, this.icon, this.allowOffhand, this.textureSize, this.renderOffsets); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java new file mode 100644 index 000000000..dd4ae01de --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.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.item; + +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.util.TriState; + +import java.util.OptionalInt; + +public record GeyserCustomItemOptions(TriState unbreakable, + OptionalInt customModelData, + OptionalInt damagePredicate) implements CustomItemOptions { + + public static class CustomItemOptionsBuilder implements CustomItemOptions.Builder { + private TriState unbreakable = TriState.NOT_SET; + private OptionalInt customModelData = OptionalInt.empty(); + private OptionalInt damagePredicate = OptionalInt.empty(); + + @Override + public Builder unbreakable(boolean unbreakable) { + if (unbreakable) { + this.unbreakable = TriState.TRUE; + } else { + this.unbreakable = TriState.FALSE; + } + return this; + } + + @Override + public Builder customModelData(int customModelData) { + this.customModelData = OptionalInt.of(customModelData); + return this; + } + + @Override + public Builder damagePredicate(int damagePredicate) { + this.damagePredicate = OptionalInt.of(damagePredicate); + return this; + } + + @Override + public CustomItemOptions build() { + return new GeyserCustomItemOptions(unbreakable, customModelData, damagePredicate); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java new file mode 100644 index 000000000..3829db3c3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomMappingData.java @@ -0,0 +1,32 @@ +/* + * 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; + +import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; +import com.nukkitx.protocol.bedrock.packet.StartGamePacket; + +public record GeyserCustomMappingData(ComponentItemData componentItemData, StartGamePacket.ItemEntry startGamePacketItemEntry, String stringId, int integerId) { +} diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java new file mode 100644 index 000000000..efdc1fdcf --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java @@ -0,0 +1,303 @@ +/* + * 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; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; +import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; +import org.jetbrains.annotations.NotNull; + +import java.util.OptionalInt; +import java.util.Set; + +@EqualsAndHashCode(callSuper = true) +@ToString +public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData implements NonVanillaCustomItemData { + private final String identifier; + private final int javaId; + private final int stackSize; + private final int maxDamage; + private final String toolType; + private final String toolTier; + private final String armorType; + private final int protectionValue; + private final String translationString; + private final Set repairMaterials; + private final OptionalInt creativeCategory; + private final String creativeGroup; + private final boolean isHat; + private final boolean isTool; + + public GeyserNonVanillaCustomItemData(NonVanillaCustomItemDataBuilder builder) { + super(builder.name, builder.customItemOptions, builder.displayName, builder.icon, builder.allowOffhand, + builder.textureSize, builder.renderOffsets); + + this.identifier = builder.identifier; + this.javaId = builder.javaId; + this.stackSize = builder.stackSize; + this.maxDamage = builder.maxDamage; + this.toolType = builder.toolType; + this.toolTier = builder.toolTier; + this.armorType = builder.armorType; + this.protectionValue = builder.protectionValue; + this.translationString = builder.translationString; + this.repairMaterials = builder.repairMaterials; + this.creativeCategory = builder.creativeCategory; + this.creativeGroup = builder.creativeGroup; + this.isHat = builder.hat; + this.isTool = builder.tool; + } + + @Override + public @NotNull String identifier() { + return identifier; + } + + @Override + public int javaId() { + return javaId; + } + + @Override + public int stackSize() { + return stackSize; + } + + @Override + public int maxDamage() { + return maxDamage; + } + + @Override + public String toolType() { + return toolType; + } + + @Override + public String toolTier() { + return toolTier; + } + + @Override + public @Nullable String armorType() { + return armorType; + } + + @Override + public int protectionValue() { + return protectionValue; + } + + @Override + public String translationString() { + return translationString; + } + + @Override + public Set repairMaterials() { + return repairMaterials; + } + + @Override + public @NotNull OptionalInt creativeCategory() { + return creativeCategory; + } + + @Override + public String creativeGroup() { + return creativeGroup; + } + + @Override + public boolean isHat() { + return isHat; + } + + @Override + public boolean isTool() { + return isTool; + } + + public static class NonVanillaCustomItemDataBuilder extends GeyserCustomItemData.CustomItemDataBuilder implements NonVanillaCustomItemData.Builder { + private String identifier = null; + private int javaId = -1; + + private int stackSize = 64; + + private int maxDamage = 0; + + private String toolType = null; + private String toolTier = null; + + private String armorType = null; + private int protectionValue = 0; + + private String translationString; + + private Set repairMaterials; + + private OptionalInt creativeCategory = OptionalInt.empty(); + private String creativeGroup = null; + + private boolean hat = false; + private boolean tool = false; + + @Override + public NonVanillaCustomItemData.Builder name(@NonNull String name) { + return (NonVanillaCustomItemData.Builder) super.name(name); + } + + @Override + public NonVanillaCustomItemData.Builder customItemOptions(@NonNull CustomItemOptions customItemOptions) { + //Do nothing, as that value won't be read + return this; + } + + @Override + public NonVanillaCustomItemData.Builder allowOffhand(boolean allowOffhand) { + return (NonVanillaCustomItemData.Builder) super.allowOffhand(allowOffhand); + } + + @Override + public NonVanillaCustomItemData.Builder displayName(@NonNull String displayName) { + return (NonVanillaCustomItemData.Builder) super.displayName(displayName); + } + + @Override + public NonVanillaCustomItemData.Builder icon(@NonNull String icon) { + return (NonVanillaCustomItemData.Builder) super.icon(icon); + } + + @Override + public NonVanillaCustomItemData.Builder textureSize(int textureSize) { + return (NonVanillaCustomItemData.Builder) super.textureSize(textureSize); + } + + @Override + public NonVanillaCustomItemData.Builder renderOffsets(CustomRenderOffsets renderOffsets) { + return (NonVanillaCustomItemData.Builder) super.renderOffsets(renderOffsets); + } + + @Override + public NonVanillaCustomItemData.Builder identifier(@NonNull String identifier) { + this.identifier = identifier; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder javaId(int javaId) { + this.javaId = javaId; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder stackSize(int stackSize) { + this.stackSize = stackSize; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder maxDamage(int maxDamage) { + this.maxDamage = maxDamage; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder toolType(@Nullable String toolType) { + this.toolType = toolType; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder toolTier(@Nullable String toolTier) { + this.toolTier = toolTier; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder armorType(@Nullable String armorType) { + this.armorType = armorType; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder protectionValue(int protectionValue) { + this.protectionValue = protectionValue; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder translationString(@Nullable String translationString) { + this.translationString = translationString; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder repairMaterials(@Nullable Set repairMaterials) { + this.repairMaterials = repairMaterials; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder creativeCategory(int creativeCategory) { + this.creativeCategory = OptionalInt.of(creativeCategory); + return this; + } + + @Override + public NonVanillaCustomItemData.Builder creativeGroup(@Nullable String creativeGroup) { + this.creativeGroup = creativeGroup; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder hat(boolean isHat) { + this.hat = isHat; + return this; + } + + @Override + public NonVanillaCustomItemData.Builder tool(boolean isTool) { + this.tool = isTool; + return this; + } + + @Override + public NonVanillaCustomItemData build() { + if (identifier == null || javaId == -1) { + throw new IllegalArgumentException("Identifier and javaId must be set"); + } + + super.customItemOptions(CustomItemOptions.builder().build()); + super.build(); + return new GeyserNonVanillaCustomItemData(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java b/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java new file mode 100644 index 000000000..6330043e5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java @@ -0,0 +1,174 @@ +/* + * 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.components; + +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtType; + +import java.util.ArrayList; +import java.util.List; + +public class ToolBreakSpeedsUtils { + public static int toolTierToSpeed(String toolTier) { + ToolTier tier = ToolTier.getByName(toolTier); + if (tier != null) { + return tier.getSpeed(); + } + + return 0; + } + + private static NbtMap createTagBreakSpeed(int speed, String... tags) { + StringBuilder builder = new StringBuilder("query.any_tag('"); + builder.append(tags[0]); + for (int i = 1; i < tags.length; i++) { + builder.append("', '").append(tags[i]); + } + builder.append("')"); + + return NbtMap.builder() + .putCompound("block", NbtMap.builder() + .putString("tags", builder.toString()) + .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", speed) + .build(); + } + + private static NbtMap createBreakSpeed(int speed, String block) { + return NbtMap.builder() + .putCompound("block", NbtMap.builder() + .putString("name", block).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", speed) + .build(); + } + + private static NbtMap createDigger(List speeds) { + return NbtMap.builder() + .putList("destroy_speeds", NbtType.COMPOUND, speeds) + .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(); + } + + public static NbtMap getAxeDigger(int speed) { + List speeds = new ArrayList<>(); + speeds.add(createTagBreakSpeed(speed, "wood", "pumpkin", "plant")); + + return createDigger(speeds); + } + + public static NbtMap getPickaxeDigger(int speed, String toolTier) { + List speeds = new ArrayList<>(); + if (toolTier.equals(ToolTier.DIAMOND.toString()) || toolTier.equals(ToolTier.NETHERITE.toString())) { + speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable", "diamond_pick_diggable")); + } else { + speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable")); + } + speeds.add(createTagBreakSpeed(speed, "stone", "metal", "rail", "mob_spawner")); + + return createDigger(speeds); + } + + public static NbtMap getShovelDigger(int speed) { + List speeds = new ArrayList<>(); + speeds.add(createTagBreakSpeed(speed, "dirt", "sand", "gravel", "grass", "snow")); + + return createDigger(speeds); + } + + public static NbtMap getSwordDigger(int speed) { + List speeds = new ArrayList<>(); + speeds.add(createBreakSpeed(speed, "minecraft:web")); + speeds.add(createBreakSpeed(speed, "minecraft:bamboo")); + + return createDigger(speeds); + } + + public static NbtMap getHoeDigger(int speed) { + List speeds = new ArrayList<>(); + speeds.add(createBreakSpeed(speed, "minecraft:leaves")); + speeds.add(createBreakSpeed(speed, "minecraft:leaves2")); + speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves")); + speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered")); + + speeds.add(createBreakSpeed(speed, "minecraft:sculk")); + speeds.add(createBreakSpeed(speed, "minecraft:sculk_catalyst")); + speeds.add(createBreakSpeed(speed, "minecraft:sculk_sensor")); + speeds.add(createBreakSpeed(speed, "minecraft:sculk_shrieker")); + speeds.add(createBreakSpeed(speed, "minecraft:sculk_vein")); + + speeds.add(createBreakSpeed(speed, "minecraft:nether_wart_block")); + speeds.add(createBreakSpeed(speed, "minecraft:warped_wart_block")); + + speeds.add(createBreakSpeed(speed, "minecraft:hay_block")); + speeds.add(createBreakSpeed(speed, "minecraft:moss_block")); + speeds.add(createBreakSpeed(speed, "minecraft:shroomlight")); + speeds.add(createBreakSpeed(speed, "minecraft:sponge")); + speeds.add(createBreakSpeed(speed, "minecraft:target")); + + return createDigger(speeds); + } + + public static NbtMap getShearsDigger(int speed) { + List speeds = new ArrayList<>(); + speeds.add(createBreakSpeed(speed, "minecraft:web")); + + speeds.add(createBreakSpeed(speed, "minecraft:leaves")); + speeds.add(createBreakSpeed(speed, "minecraft:leaves2")); + speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves")); + speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered")); + + speeds.add(createBreakSpeed(speed, "minecraft:wool")); + + speeds.add(createBreakSpeed(speed, "minecraft:glow_lichen")); + speeds.add(createBreakSpeed(speed, "minecraft:vine")); + + return createDigger(speeds); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java b/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java new file mode 100644 index 000000000..37e581682 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/components/ToolTier.java @@ -0,0 +1,66 @@ +/* + * 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.components; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Locale; + +public enum ToolTier { + WOODEN(2), + STONE(4), + IRON(6), + GOLDEN(12), + DIAMOND(8), + NETHERITE(9); + + public static final ToolTier[] VALUES = values(); + + private final int speed; + + ToolTier(int speed) { + this.speed = speed; + } + + public int getSpeed() { + return speed; + } + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } + + public static ToolTier getByName(@NonNull String name) { + String upperCase = name.toUpperCase(Locale.ROOT); + for (ToolTier tier : VALUES) { + if (tier.name().equals(upperCase)) { + return tier; + } + } + return null; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/components/WearableSlot.java b/core/src/main/java/org/geysermc/geyser/item/components/WearableSlot.java new file mode 100644 index 000000000..a4479f871 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/components/WearableSlot.java @@ -0,0 +1,47 @@ +/* + * 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.components; + +import com.nukkitx.nbt.NbtMap; + +import java.util.Locale; + +public enum WearableSlot { + HEAD, + CHEST, + LEGS, + FEET; + + private final NbtMap slotNbt; + + WearableSlot() { + this.slotNbt = NbtMap.builder().putString("slot", "slot.armor." + this.name().toLowerCase(Locale.ROOT)).build(); + } + + public NbtMap getSlotNbt() { + return slotNbt; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/exception/InvalidCustomMappingsFileException.java b/core/src/main/java/org/geysermc/geyser/item/exception/InvalidCustomMappingsFileException.java new file mode 100644 index 000000000..5878f5cc7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/exception/InvalidCustomMappingsFileException.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.item.exception; + +public class InvalidCustomMappingsFileException extends Exception { + public InvalidCustomMappingsFileException(Throwable cause) { + super(cause); + } + + public InvalidCustomMappingsFileException(String message) { + super(message); + } + + public InvalidCustomMappingsFileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java b/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java new file mode 100644 index 000000000..eaf07c382 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java @@ -0,0 +1,98 @@ +/* + * 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; + +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +public class MappingsConfigReader { + private final Int2ObjectMap mappingReaders = new Int2ObjectOpenHashMap<>(); + private final Path customMappingsDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("custom_mappings"); + + public MappingsConfigReader() { + this.mappingReaders.put(1, new MappingsReader_v1()); + } + + public Path[] getCustomMappingsFiles() { + try { + return Files.walk(this.customMappingsDirectory) + .filter(child -> child.toString().endsWith(".json")) + .toArray(Path[]::new); + } catch (IOException e) { + return new Path[0]; + } + } + + public void loadMappingsFromJson(BiConsumer consumer) { + Path customMappingsDirectory = this.customMappingsDirectory; + if (!Files.exists(customMappingsDirectory)) { + try { + Files.createDirectories(customMappingsDirectory); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to create custom mappings directory", e); + return; + } + } + + Path[] mappingsFiles = this.getCustomMappingsFiles(); + for (Path mappingsFile : mappingsFiles) { + this.readMappingsFromJson(mappingsFile, consumer); + } + } + + public void readMappingsFromJson(Path file, BiConsumer consumer) { + 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; + } + + if (!mappingsRoot.has("format_version")) { + GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " is missing the format version field!"); + return; + } + + 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; + } + + this.mappingReaders.get(formatVersion).readMappings(file, mappingsRoot, consumer); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java b/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java new file mode 100644 index 000000000..ef553f488 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java @@ -0,0 +1,93 @@ +/* + * 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.api.item.custom.CustomItemData; +import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; +import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; + +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 CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException; + + protected CustomRenderOffsets fromJsonNode(JsonNode node) { + if (node == null || !node.isObject()) { + return null; + } + + return new CustomRenderOffsets( + getHandOffsets(node, "main_hand"), + getHandOffsets(node, "off_hand") + ); + } + + protected CustomRenderOffsets.Hand getHandOffsets(JsonNode node, String hand) { + JsonNode tmpNode = node.get(hand); + if (tmpNode == null || !tmpNode.isObject()) { + return null; + } + + return new CustomRenderOffsets.Hand( + getPerspectiveOffsets(tmpNode, "first_person"), + getPerspectiveOffsets(tmpNode, "third_person") + ); + } + + protected CustomRenderOffsets.Offset getPerspectiveOffsets(JsonNode node, String perspective) { + JsonNode tmpNode = node.get(perspective); + if (tmpNode == null || !tmpNode.isObject()) { + return null; + } + + return new CustomRenderOffsets.Offset( + getOffsetXYZ(tmpNode, "position"), + getOffsetXYZ(tmpNode, "rotation"), + getOffsetXYZ(tmpNode, "scale") + ); + } + + protected CustomRenderOffsets.OffsetXYZ getOffsetXYZ(JsonNode node, String offsetType) { + JsonNode tmpNode = node.get(offsetType); + if (tmpNode == null || !tmpNode.isObject()) { + return null; + } + + if (!tmpNode.has("x") || !tmpNode.has("y") || !tmpNode.has("z")) { + return null; + } + + return new CustomRenderOffsets.OffsetXYZ( + tmpNode.get("x").floatValue(), + tmpNode.get("y").floatValue(), + tmpNode.get("z").floatValue() + ); + } +} 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 new file mode 100644 index 000000000..217ff844e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java @@ -0,0 +1,123 @@ +/* + * 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()); + } + + return customItemOptions.build(); + } + + @Override + public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException { + if (node == null || !node.isObject()) { + throw new InvalidCustomMappingsFileException("Invalid item mappings entry"); + } + + String name = node.get("name").asText(); + if (name == null || name.isEmpty()) { + throw new InvalidCustomMappingsFileException("An item entry has no name"); + } + + CustomItemData.Builder customItemData = CustomItemData.builder() + .name(name) + .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("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/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 6030b6ebf..33eca67a9 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -31,6 +31,7 @@ import com.nukkitx.protocol.bedrock.data.ExperimentData; import com.nukkitx.protocol.bedrock.data.ResourcePackType; import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.pack.ResourcePack; @@ -38,7 +39,6 @@ import org.geysermc.geyser.pack.ResourcePackManifest; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; @@ -160,7 +160,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.getUuid().toString(), header.getVersionString(), "")); } - if (session.getItemMappings().getFurnaceMinecartData() != null) { + if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { // Allow custom items to work stackPacket.getExperiments().add(new ExperimentData("data_driven_items", true)); } diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java index 08d6b5738..bef5c7418 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/ResourcePack.java @@ -26,12 +26,16 @@ package org.geysermc.geyser.pack; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.text.GeyserLocale; import java.io.File; -import java.util.HashMap; -import java.util.Map; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -59,16 +63,33 @@ public class ResourcePack { * Loop through the packs directory and locate valid resource pack files */ public static void loadPacks() { - File directory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("packs").toFile(); + Path directory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("packs"); - if (!directory.exists()) { - directory.mkdir(); + if (!Files.exists(directory)) { + try { + Files.createDirectory(directory); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not create packs directory", e); + } // As we just created the directory it will be empty return; } - for (File file : directory.listFiles()) { + List resourcePacks; + try { + resourcePacks = Files.walk(directory).collect(Collectors.toList()); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Could not list packs directory", e); + return; + } + + GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); + GeyserImpl.getInstance().eventBus().fire(event); + + for (Path path : event.resourcePacks()) { + File file = path.toFile(); + if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) { ResourcePack pack = new ResourcePack(); 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 9c370bc1c..f1dd054f5 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -113,11 +113,6 @@ public final class Registries { */ public static final SimpleMappedRegistry> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(() -> new EnumMap<>(EntityType.class))); - /** - * A map containing all the extension loaders. - */ - public static final SimpleMappedRegistry EXTENSION_LOADERS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); - /** * A map containing all Java entity identifiers and their respective Geyser definitions */ 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 new file mode 100644 index 000000000..64543272e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -0,0 +1,360 @@ +/* + * 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 com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.nbt.NbtType; +import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; +import com.nukkitx.protocol.bedrock.packet.StartGamePacket; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.item.custom.CustomItemData; +import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; +import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; +import org.geysermc.geyser.item.GeyserCustomMappingData; +import org.geysermc.geyser.item.components.ToolBreakSpeedsUtils; +import org.geysermc.geyser.item.components.WearableSlot; +import org.geysermc.geyser.registry.type.GeyserMappingItem; +import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.registry.type.NonVanillaItemRegistration; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +public class CustomItemRegistryPopulator { + public static GeyserCustomMappingData registerCustomItem(String customItemName, GeyserMappingItem javaItem, CustomItemData customItemData, int bedrockId) { + StartGamePacket.ItemEntry startGamePacketItemEntry = new StartGamePacket.ItemEntry(customItemName, (short) bedrockId, true); + + NbtMapBuilder builder = createComponentNbt(customItemData, javaItem, customItemName, bedrockId); + ComponentItemData componentItemData = new ComponentItemData(customItemName, builder.build()); + + return new GeyserCustomMappingData(componentItemData, startGamePacketItemEntry, customItemName, bedrockId); + } + + static boolean initialCheck(String identifier, CustomItemData item, Map mappings) { + if (!mappings.containsKey(identifier)) { + GeyserImpl.getInstance().getLogger().error("Could not find the Java item to add custom item properties to for " + item.name()); + return false; + } + if (!item.customItemOptions().hasCustomItemOptions()) { + GeyserImpl.getInstance().getLogger().error("The custom item " + item.name() + " has no registration types"); + } + return true; + } + + public static NonVanillaItemRegistration registerCustomItem(NonVanillaCustomItemData customItemData, int customItemId) { + String customIdentifier = customItemData.identifier(); + + ItemMapping customItemMapping = ItemMapping.builder() + .javaIdentifier(customIdentifier) + .bedrockIdentifier(customIdentifier) + .javaId(customItemData.javaId()) + .bedrockId(customItemId) + .bedrockData(0) + .bedrockBlockId(0) + .stackSize(customItemData.stackSize()) + .toolType(customItemData.toolType()) + .toolTier(customItemData.toolTier()) + .translationString(customItemData.translationString()) + .maxDamage(customItemData.maxDamage()) + .repairMaterials(customItemData.repairMaterials()) + .hasSuspiciousStewEffect(false) + .customItemOptions(Object2IntMaps.emptyMap()) + .build(); + + NbtMapBuilder builder = createComponentNbt(customItemData, customItemData.identifier(), customItemId, + customItemData.creativeCategory(), customItemData.creativeGroup(), customItemData.isHat(), customItemData.isTool()); + ComponentItemData componentItemData = new ComponentItemData(customIdentifier, builder.build()); + + return new NonVanillaItemRegistration(componentItemData, customItemMapping); + } + + private static NbtMapBuilder createComponentNbt(CustomItemData customItemData, GeyserMappingItem mapping, + String customItemName, int customItemId) { + NbtMapBuilder builder = NbtMap.builder(); + builder.putString("name", customItemName) + .putInt("id", customItemId); + + NbtMapBuilder itemProperties = NbtMap.builder(); + NbtMapBuilder componentBuilder = NbtMap.builder(); + + setupBasicItemInfo(mapping.getMaxDamage(), mapping.getStackSize(), mapping.getToolType() != null, customItemData, itemProperties, componentBuilder); + + boolean canDestroyInCreative = true; + if (mapping.getToolType() != null) { // This is not using the isTool boolean because it is not just a render type here. + canDestroyInCreative = computeToolProperties(mapping.getToolTier(), mapping.getToolType(), itemProperties, componentBuilder); + } + itemProperties.putBoolean("can_destroy_in_creative", canDestroyInCreative); + + if (mapping.getArmorType() != null) { + computeArmorProperties(mapping.getArmorType(), mapping.getProtectionValue(), componentBuilder); + } + + computeRenderOffsets(false, customItemData, componentBuilder); + + componentBuilder.putCompound("item_properties", itemProperties.build()); + builder.putCompound("components", componentBuilder.build()); + + return builder; + } + + private static NbtMapBuilder createComponentNbt(NonVanillaCustomItemData customItemData, String customItemName, + int customItemId, OptionalInt creativeCategory, + String creativeGroup, boolean isHat, boolean isTool) { + NbtMapBuilder builder = NbtMap.builder(); + builder.putString("name", customItemName) + .putInt("id", customItemId); + + NbtMapBuilder itemProperties = NbtMap.builder(); + NbtMapBuilder componentBuilder = NbtMap.builder(); + + setupBasicItemInfo(customItemData.maxDamage(), customItemData.stackSize(), isTool, customItemData, itemProperties, componentBuilder); + + boolean canDestroyInCreative = true; + if (customItemData.toolType() != null) { // This is not using the isTool boolean because it is not just a render type here. + canDestroyInCreative = computeToolProperties(customItemData.toolTier(), customItemData.toolType(), itemProperties, componentBuilder); + } + itemProperties.putBoolean("can_destroy_in_creative", canDestroyInCreative); + + String armorType = customItemData.armorType(); + if (armorType != null) { + computeArmorProperties(armorType, customItemData.protectionValue(), componentBuilder); + } + + computeRenderOffsets(isHat, customItemData, componentBuilder); + + if (creativeGroup != null) { + itemProperties.putString("creative_group", creativeGroup); + } + if (creativeCategory.isPresent()) { + itemProperties.putInt("creative_category", creativeCategory.getAsInt()); + } + + componentBuilder.putCompound("item_properties", itemProperties.build()); + builder.putCompound("components", componentBuilder.build()); + + return builder; + } + + private static void setupBasicItemInfo(int maxDamage, int stackSize, boolean isTool, CustomItemData customItemData, NbtMapBuilder itemProperties, NbtMapBuilder componentBuilder) { + itemProperties.putCompound("minecraft:icon", NbtMap.builder() + .putString("texture", customItemData.icon()) + .build()); + componentBuilder.putCompound("minecraft:display_name", NbtMap.builder().putString("value", customItemData.displayName()).build()); + + itemProperties.putBoolean("allow_off_hand", customItemData.allowOffhand()); + itemProperties.putBoolean("hand_equipped", isTool); + itemProperties.putInt("max_stack_size", stackSize); + if (maxDamage > 0) { + componentBuilder.putCompound("minecraft:durability", NbtMap.builder() + .putCompound("damage_chance", NbtMap.builder() + .putInt("max", 1) + .putInt("min", 1) + .build()) + .putInt("max_durability", maxDamage) + .build()); + itemProperties.putBoolean("use_duration", true); + } + } + + /** + * @return can destroy in creative + */ + private static boolean computeToolProperties(String toolTier, String toolType, NbtMapBuilder itemProperties, NbtMapBuilder componentBuilder) { + 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"); + } + } + } + + itemProperties.putBoolean("hand_equipped", true); + itemProperties.putFloat("mining_speed", miningSpeed); + + return canDestroyInCreative; + } + + private static void computeArmorProperties(String armorType, int protectionValue, NbtMapBuilder componentBuilder) { + switch (armorType) { + case "boots" -> { + componentBuilder.putString("minecraft:render_offsets", "boots"); + componentBuilder.putCompound("minecraft:wearable", WearableSlot.FEET.getSlotNbt()); + componentBuilder.putCompound("minecraft:armor", NbtMap.builder().putInt("protection", protectionValue).build()); + } + case "chestplate" -> { + componentBuilder.putString("minecraft:render_offsets", "chestplates"); + componentBuilder.putCompound("minecraft:wearable", WearableSlot.CHEST.getSlotNbt()); + componentBuilder.putCompound("minecraft:armor", NbtMap.builder().putInt("protection", protectionValue).build()); + } + case "leggings" -> { + componentBuilder.putString("minecraft:render_offsets", "leggings"); + componentBuilder.putCompound("minecraft:wearable", WearableSlot.LEGS.getSlotNbt()); + componentBuilder.putCompound("minecraft:armor", NbtMap.builder().putInt("protection", protectionValue).build()); + } + case "helmet" -> { + componentBuilder.putString("minecraft:render_offsets", "helmets"); + componentBuilder.putCompound("minecraft:wearable", WearableSlot.HEAD.getSlotNbt()); + componentBuilder.putCompound("minecraft:armor", NbtMap.builder().putInt("protection", protectionValue).build()); + } + } + } + + private static void computeRenderOffsets(boolean isHat, CustomItemData customItemData, NbtMapBuilder componentBuilder) { + if (isHat) { + componentBuilder.remove("minecraft:render_offsets"); + componentBuilder.putString("minecraft:render_offsets", "helmets"); + + componentBuilder.remove("minecraft:wearable"); + componentBuilder.putCompound("minecraft:wearable", WearableSlot.HEAD.getSlotNbt()); + } + + CustomRenderOffsets renderOffsets = customItemData.renderOffsets(); + if (renderOffsets != null) { + componentBuilder.remove("minecraft:render_offsets"); + componentBuilder.putCompound("minecraft:render_offsets", toNbtMap(renderOffsets)); + } else if (customItemData.textureSize() != 16 && !componentBuilder.containsKey("minecraft:render_offsets")) { + float scale1 = (float) (0.075 / (customItemData.textureSize() / 16f)); + float scale2 = (float) (0.125 / (customItemData.textureSize() / 16f)); + float scale3 = (float) (0.075 / (customItemData.textureSize() / 16f * 2.4f)); + + componentBuilder.putCompound("minecraft:render_offsets", + NbtMap.builder().putCompound("main_hand", NbtMap.builder() + .putCompound("first_person", xyzToScaleList(scale3, scale3, scale3)) + .putCompound("third_person", xyzToScaleList(scale1, scale2, scale1)).build()) + .putCompound("off_hand", NbtMap.builder() + .putCompound("first_person", xyzToScaleList(scale1, scale2, scale1)) + .putCompound("third_person", xyzToScaleList(scale1, scale2, scale1)).build()).build()); + } + } + + private static NbtMap toNbtMap(CustomRenderOffsets renderOffsets) { + NbtMapBuilder builder = NbtMap.builder(); + + CustomRenderOffsets.Hand mainHand = renderOffsets.mainHand(); + if (mainHand != null) { + NbtMap nbt = toNbtMap(mainHand); + if (nbt != null) { + builder.putCompound("main_hand", nbt); + } + } + CustomRenderOffsets.Hand offhand = renderOffsets.offhand(); + if (offhand != null) { + NbtMap nbt = toNbtMap(offhand); + if (nbt != null) { + builder.putCompound("off_hand", nbt); + } + } + + return builder.build(); + } + + private static NbtMap toNbtMap(CustomRenderOffsets.Hand hand) { + NbtMap firstPerson = toNbtMap(hand.firstPerson()); + NbtMap thirdPerson = toNbtMap(hand.thirdPerson()); + + if (firstPerson == null && thirdPerson == null) { + return null; + } + + NbtMapBuilder builder = NbtMap.builder(); + if (firstPerson != null) { + builder.putCompound("first_person", firstPerson); + } + if (thirdPerson != null) { + builder.putCompound("third_person", thirdPerson); + } + + return builder.build(); + } + + private static NbtMap toNbtMap(@Nullable CustomRenderOffsets.Offset offset) { + if (offset == null) { + return null; + } + + CustomRenderOffsets.OffsetXYZ position = offset.position(); + CustomRenderOffsets.OffsetXYZ rotation = offset.rotation(); + CustomRenderOffsets.OffsetXYZ scale = offset.scale(); + + if (position == null && rotation == null && scale == null) { + return null; + } + + NbtMapBuilder builder = NbtMap.builder(); + if (position != null) { + builder.putList("position", NbtType.FLOAT, toList(position)); + } + if (rotation != null) { + builder.putList("rotation", NbtType.FLOAT, toList(rotation)); + } + if (scale != null) { + builder.putList("scale", NbtType.FLOAT, toList(scale)); + } + + return builder.build(); + } + + private static List toList(CustomRenderOffsets.OffsetXYZ xyz) { + return List.of(xyz.x(), xyz.y(), xyz.z()); + } + + private static void setItemTag(NbtMapBuilder builder, String tag) { + builder.putList("item_tags", NbtType.STRING, List.of("minecraft:is_" + tag)); + } + + private static NbtMap xyzToScaleList(float x, float y, float z) { + return NbtMap.builder().putList("scale", NbtType.FLOAT, List.of(x, y, z)).build(); + } +} 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 f3d936b2e..22669fd79 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 @@ -27,6 +27,8 @@ package org.geysermc.geyser.registry.populator; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.nbt.NbtType; @@ -35,14 +37,22 @@ import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.StartGamePacket; +import it.unimi.dsi.fastutil.ints.*; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.objects.*; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomItemsEvent; +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.mappings.MappingsConfigReader; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.*; @@ -78,6 +88,58 @@ public class ItemRegistryPopulator { throw new AssertionError("Unable to load Java runtime item IDs", e); } + boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems(); + + Multimap customItems = MultimapBuilder.hashKeys().hashSetValues().build(); + List nonVanillaCustomItems; + + MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); + if (customItemsAllowed) { + // Load custom items from mappings files + mappingsConfigReader.loadMappingsFromJson((key, item) -> { + if (CustomItemRegistryPopulator.initialCheck(key, item, items)) { + customItems.get(key).add(item); + } + }); + + nonVanillaCustomItems = new ObjectArrayList<>(); + GeyserImpl.getInstance().eventBus().fire(new GeyserDefineCustomItemsEvent(customItems, nonVanillaCustomItems) { + @Override + public boolean register(@NonNull String identifier, @NonNull CustomItemData customItemData) { + if (CustomItemRegistryPopulator.initialCheck(identifier, customItemData, items)) { + customItems.get(identifier).add(customItemData); + return true; + } + return false; + } + + @Override + public boolean register(@NonNull NonVanillaCustomItemData customItemData) { + if (customItemData.identifier().startsWith("minecraft:")) { + GeyserImpl.getInstance().getLogger().error("The custom item " + customItemData.identifier() + + " is attempting to masquerade as a vanilla Minecraft item!"); + return false; + } + + if (customItemData.javaId() < items.size()) { + // Attempting to overwrite an item that already exists in the protocol + GeyserImpl.getInstance().getLogger().error("The custom item " + customItemData.identifier() + + " is attempting to overwrite a vanilla Minecraft item!"); + return false; + } + nonVanillaCustomItems.add(customItemData); + return true; + } + }); + } else { + nonVanillaCustomItems = Collections.emptyList(); + } + + int customItemCount = customItems.size() + nonVanillaCustomItems.size(); + if (customItemCount > 0) { + GeyserImpl.getInstance().getLogger().info("Registered " + customItemCount + " custom items"); + } + // We can reduce some operations as Java information is the same across all palette versions boolean firstMappingsPass = true; Int2IntMap dyeColors = new FixedInt2IntMap(); @@ -99,11 +161,20 @@ public class ItemRegistryPopulator { throw new AssertionError("Unable to load Bedrock runtime item IDs", e); } + // Used for custom items + int nextFreeBedrockId = 0; + List componentItemData = new ObjectArrayList<>(); + Map entries = new Object2ObjectOpenHashMap<>(); for (PaletteItem entry : itemEntries) { - entries.put(entry.getName(), new StartGamePacket.ItemEntry(entry.getName(), (short) entry.getId())); - bedrockIdentifierToId.put(entry.getName(), entry.getId()); + int id = entry.getId(); + if (id >= nextFreeBedrockId) { + nextFreeBedrockId = id + 1; + } + + entries.put(entry.getName(), new StartGamePacket.ItemEntry(entry.getName(), (short) id)); + bedrockIdentifierToId.put(entry.getName(), id); } Object2IntMap bedrockBlockIdOverrides = new Object2IntOpenHashMap<>(); @@ -203,18 +274,20 @@ public class ItemRegistryPopulator { int itemIndex = 0; int javaFurnaceMinecartId = 0; - boolean usingFurnaceMinecart = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems(); Set javaOnlyItems = new ObjectOpenHashSet<>(); Collections.addAll(javaOnlyItems, "minecraft:spectral_arrow", "minecraft:debug_stick", "minecraft:knowledge_book", "minecraft:tipped_arrow", "minecraft:trader_llama_spawn_egg", "minecraft:bundle"); - if (!usingFurnaceMinecart) { + if (!customItemsAllowed) { javaOnlyItems.add("minecraft:furnace_minecart"); } // Java-only items for this version javaOnlyItems.addAll(palette.getValue().additionalTranslatedItems().keySet()); + Int2ObjectMap customIdMappings = new Int2ObjectOpenHashMap<>(); + Set registeredItemNames = new ObjectOpenHashSet<>(); // This is used to check for duplicate item names + for (Map.Entry entry : items.entrySet()) { String javaIdentifier = entry.getKey().intern(); GeyserMappingItem mappingItem; @@ -226,7 +299,7 @@ public class ItemRegistryPopulator { mappingItem = entry.getValue(); } - if (usingFurnaceMinecart && javaIdentifier.equals("minecraft:furnace_minecart")) { + if (customItemsAllowed && javaIdentifier.equals("minecraft:furnace_minecart")) { javaFurnaceMinecartId = itemIndex; itemIndex++; // Will be added later @@ -380,12 +453,46 @@ public class ItemRegistryPopulator { .toolTier(""); } } + if (javaOnlyItems.contains(javaIdentifier)) { // These items don't exist on Bedrock, so set up a variable that indicates they should have custom names mappingBuilder = mappingBuilder.translationString((bedrockBlockId != -1 ? "block." : "item.") + entry.getKey().replace(":", ".")); GeyserImpl.getInstance().getLogger().debug("Adding " + entry.getKey() + " as an item that needs to be translated."); } + // Add the custom item properties, if applicable + Object2IntMap customItemOptions; + Collection customItemsToLoad = customItems.get(javaIdentifier); + if (customItemsAllowed && !customItemsToLoad.isEmpty()) { + customItemOptions = new Object2IntOpenHashMap<>(customItemsToLoad.size()); + + for (CustomItemData customItem : customItemsToLoad) { + int customProtocolId = nextFreeBedrockId++; + + String customItemName = "geyser_custom:" + customItem.name(); + if (!registeredItemNames.add(customItemName)) { + if (firstMappingsPass) { + GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItem.name() + "' already exists and was registered again! Skipping..."); + } + continue; + } + + GeyserCustomMappingData customMapping = CustomItemRegistryPopulator.registerCustomItem( + customItemName, mappingItem, customItem, customProtocolId + ); + // StartGamePacket entry - needed for Bedrock to recognize the item through the protocol + entries.put(customMapping.stringId(), customMapping.startGamePacketItemEntry()); + // ComponentItemData - used to register some custom properties + componentItemData.add(customMapping.componentItemData()); + customItemOptions.put(customItem.customItemOptions(), customProtocolId); + + customIdMappings.put(customMapping.integerId(), customMapping.stringId()); + } + } else { + customItemOptions = Object2IntMaps.emptyMap(); + } + mappingBuilder.customItemOptions(customItemOptions); + ItemMapping mapping = mappingBuilder.build(); if (javaIdentifier.contains("boat")) { @@ -436,12 +543,12 @@ public class ItemRegistryPopulator { .bedrockData(0) .bedrockBlockId(-1) .stackSize(1) + .customItemOptions(Object2IntMaps.emptyMap()) .build(); - ComponentItemData furnaceMinecartData = null; - if (usingFurnaceMinecart) { + if (customItemsAllowed) { // Add the furnace minecart as a custom item - int furnaceMinecartId = mappings.size() + 1; + int furnaceMinecartId = nextFreeBedrockId++; entries.put("geysermc:furnace_minecart", new StartGamePacket.ItemEntry("geysermc:furnace_minecart", (short) furnaceMinecartId, true)); @@ -456,7 +563,7 @@ public class ItemRegistryPopulator { .build()); creativeItems.add(ItemData.builder() - .netId(netId) + .netId(netId++) .id(furnaceMinecartId) .count(1).build()); @@ -492,7 +599,36 @@ public class ItemRegistryPopulator { componentBuilder.putCompound("item_properties", itemProperties.build()); builder.putCompound("components", componentBuilder.build()); - furnaceMinecartData = new ComponentItemData("geysermc:furnace_minecart", builder.build()); + componentItemData.add(new ComponentItemData("geysermc:furnace_minecart", builder.build())); + + // Register any completely custom items given to us + IntSet registeredJavaIds = new IntOpenHashSet(); // Used to check for duplicate item java ids + for (NonVanillaCustomItemData customItem : nonVanillaCustomItems) { + if (!registeredJavaIds.add(customItem.javaId())) { + if (firstMappingsPass) { + GeyserImpl.getInstance().getLogger().error("Custom item java id " + customItem.javaId() + " already exists and was registered again! Skipping..."); + } + continue; + } + + int customItemId = nextFreeBedrockId++; + NonVanillaItemRegistration registration = CustomItemRegistryPopulator.registerCustomItem(customItem, customItemId); + + componentItemData.add(registration.componentItemData()); + ItemMapping mapping = registration.mapping(); + while (mapping.getJavaId() >= mappings.size()) { + // Fill with empty to get to the correct size + mappings.add(ItemMapping.AIR); + } + mappings.set(mapping.getJavaId(), mapping); + + if (customItem.creativeGroup() != null || customItem.creativeCategory().isPresent()) { + creativeItems.add(ItemData.builder() + .id(customItemId) + .netId(netId++) + .count(1).build()); + } + } } ItemMappings itemMappings = ItemMappings.builder() @@ -506,8 +642,9 @@ public class ItemRegistryPopulator { .boatIds(boats) .spawnEggIds(spawnEggs) .carpets(carpets) - .furnaceMinecartData(furnaceMinecartData) + .componentItemData(componentItemData) .lodestoneCompass(lodestoneEntry) + .customIdMappings(customIdMappings) .build(); Registries.ITEMS.register(palette.getValue().protocolVersion(), itemMappings); diff --git a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java index e144fcd4f..af289bcda 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java +++ b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java @@ -29,8 +29,14 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.command.CommandSource; +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.api.provider.BuilderProvider; import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.item.GeyserCustomItemData; +import org.geysermc.geyser.item.GeyserCustomItemOptions; +import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; import org.geysermc.geyser.registry.ProviderRegistries; import org.geysermc.geyser.registry.SimpleMappedRegistry; @@ -46,6 +52,9 @@ public class GeyserBuilderProvider extends AbstractProvider implements BuilderPr @Override public void registerProviders(Map, ProviderSupplier> providers) { providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Class) args[0])); + providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder()); + providers.put(CustomItemOptions.Builder.class, args -> new GeyserCustomItemOptions.CustomItemOptionsBuilder()); + providers.put(NonVanillaCustomItemData.Builder.class, args -> new GeyserNonVanillaCustomItemData.NonVanillaCustomItemDataBuilder()); } @Override 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 9d06fd3a9..6c65f1c34 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 @@ -42,6 +42,8 @@ public class GeyserMappingItem { @JsonProperty("stack_size") int stackSize = 64; @JsonProperty("tool_type") String toolType; @JsonProperty("tool_tier") String toolTier; + @JsonProperty("armor_type") String armorType; + @JsonProperty("protection_value") int protectionValue; @JsonProperty("max_damage") int maxDamage = 0; @JsonProperty("repair_materials") List repairMaterials; @JsonProperty("has_suspicious_stew_effect") boolean hasSuspiciousStewEffect = false; diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java index 332ab0167..12ba7d208 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java @@ -25,9 +25,12 @@ package org.geysermc.geyser.registry.type; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Value; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.registry.BlockRegistries; @@ -39,7 +42,7 @@ import java.util.Set; public class ItemMapping { public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0, BlockRegistries.BLOCKS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(), - 64, null, null, null, 0, null, false); + 64, null, null, null, Object2IntMaps.emptyMap(), 0, null, false); String javaIdentifier; String bedrockIdentifier; @@ -59,6 +62,8 @@ public class ItemMapping { String translationString; + Object2IntMap customItemOptions; + int maxDamage; Set repairMaterials; @@ -91,4 +96,4 @@ public class ItemMapping { public boolean isTool() { return this.toolType != null; } -} \ No newline at end of file +} 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 3072568f3..c4e967dff 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 @@ -29,6 +29,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.StartGamePacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.IntList; import lombok.Builder; import lombok.Value; @@ -36,7 +37,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.item.StoredItemMappings; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Set; @@ -67,7 +67,8 @@ public class ItemMappings { IntList spawnEggIds; List carpets; - @Nullable ComponentItemData furnaceMinecartData; + List componentItemData; + Int2ObjectMap customIdMappings; /** * Gets an {@link ItemMapping} from the given {@link ItemStack}. diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/NonVanillaItemRegistration.java b/core/src/main/java/org/geysermc/geyser/registry/type/NonVanillaItemRegistration.java new file mode 100644 index 000000000..e2063f41a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/type/NonVanillaItemRegistration.java @@ -0,0 +1,34 @@ +/* + * 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 com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; + +/** + * The return data of a successful registration of a custom item. + */ +public record NonVanillaItemRegistration(ComponentItemData componentItemData, ItemMapping mapping) { +} \ No newline at end of file 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 49261500b..99e29dd21 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -594,9 +594,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { // Set the hardcoded shield ID to the ID we just defined in StartGamePacket upstream.getSession().getHardcodedBlockingId().set(this.itemMappings.getStoredItems().shield().getBedrockId()); - if (this.itemMappings.getFurnaceMinecartData() != null) { + if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { ItemComponentPacket componentPacket = new ItemComponentPacket(); - componentPacket.getItems().add(this.itemMappings.getFurnaceMinecartData()); + componentPacket.getItems().addAll(itemMappings.getComponentItemData()); upstream.sendPacket(componentPacket); } @@ -1465,7 +1465,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { // startGamePacket.setCurrentTick(0); startGamePacket.setEnchantmentSeed(0); startGamePacket.setMultiplayerCorrelationId(""); + startGamePacket.setItemEntries(this.itemMappings.getItemEntries()); + startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); startGamePacket.setServerEngine(""); // Do we want to fill this in? diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CompassTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CompassTranslator.java index b5778e681..a0da82648 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CompassTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CompassTranslator.java @@ -79,8 +79,7 @@ public class CompassTranslator extends ItemTranslator { @Override public List getAppliedItems() { - return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) - .getItems()) + return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getItems()) .filter(entry -> entry.getJavaIdentifier().endsWith("compass")) .collect(Collectors.toList()); } 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 b9bfdd576..0a2ab57df 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 @@ -34,9 +34,12 @@ import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.ItemMapping; @@ -122,7 +125,7 @@ public abstract class ItemTranslator { } } if (itemStack.getNbt().isEmpty()) { - // Otherwise, seems to causes issues with villagers accepting books, and I don't see how this will break anything else. - Camotoy + // Otherwise, seems to cause issues with villagers accepting books, and I don't see how this will break anything else. - Camotoy itemStack = new ItemStack(itemStack.getId(), itemStack.getAmount(), null); } } @@ -170,6 +173,8 @@ public abstract class ItemTranslator { builder.blockRuntimeId(bedrockItem.getBedrockBlockId()); } + translateCustomItem(nbt, builder, bedrockItem); + if (nbt != null) { // Translate the canDestroy and canPlaceOn Java NBT ListTag canDestroy = nbt.get("CanDestroy"); @@ -292,6 +297,10 @@ public abstract class ItemTranslator { if (itemStack.getNbt() != null) { builder.tag(this.translateNbtToBedrock(itemStack.getNbt())); } + + CompoundTag nbt = itemStack.getNbt(); + translateCustomItem(nbt, builder, mapping); + return builder; } @@ -416,7 +425,7 @@ public abstract class ItemTranslator { if (object instanceof byte[]) { return new ByteArrayTag(name, (byte[]) object); } - + if (object instanceof Byte) { return new ByteTag(name, (byte) object); } @@ -524,6 +533,48 @@ public abstract class ItemTranslator { return tag; } + /** + * Translates the custom model data of an item + */ + private static void translateCustomItem(CompoundTag nbt, ItemData.Builder builder, ItemMapping mapping) { + int bedrockId = getCustomItem(nbt, mapping); + if (bedrockId != -1) { + builder.id(bedrockId); + } + } + + private static int getCustomItem(CompoundTag nbt, ItemMapping mapping) { + if (nbt == null) { + return -1; + } + Object2IntMap customMappings = mapping.getCustomItemOptions(); + if (customMappings.isEmpty()) { + return -1; + } + int customModelData = nbt.get("CustomModelData") instanceof IntTag customModelDataTag ? customModelDataTag.getValue() : 0; + TriState unbreakable = TriState.fromBoolean(nbt.get("Unbreakable") instanceof ByteTag unbreakableTag && unbreakableTag.getValue() == 1); + int damage = nbt.get("Damage") instanceof IntTag damageTag ? damageTag.getValue() : 0; + for (Object2IntMap.Entry mappingTypes : customMappings.object2IntEntrySet()) { + CustomItemOptions options = mappingTypes.getKey(); + + TriState unbreakableOption = options.unbreakable(); + if (unbreakableOption == unbreakable) { // Implementation note: if the option is NOT_SET then this comparison will always be false because of how the item unbreaking TriState is created + return mappingTypes.getIntValue(); + } + + OptionalInt customModelDataOption = options.customModelData(); + if (customModelDataOption.isPresent() && customModelDataOption.getAsInt() == customModelData) { + return mappingTypes.getIntValue(); + } + + OptionalInt damagePredicate = options.damagePredicate(); + if (damagePredicate.isPresent() && damagePredicate.getAsInt() == damage) { + return mappingTypes.getIntValue(); + } + } + return -1; + } + /** * Checks if an {@link ItemStack} is equal to another item stack * diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java index f12355ce6..3e814a098 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/PotionTranslator.java @@ -74,8 +74,7 @@ public class PotionTranslator extends ItemTranslator { @Override public List getAppliedItems() { - return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) - .getItems()) + return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getItems()) .filter(entry -> entry.getJavaIdentifier().endsWith("potion")) .collect(Collectors.toList()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/TippedArrowTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/TippedArrowTranslator.java index 5dc525e56..bbf598ecd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/TippedArrowTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/TippedArrowTranslator.java @@ -81,8 +81,7 @@ public class TippedArrowTranslator extends ItemTranslator { @Override public List getAppliedItems() { - return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) - .getItems()) + return Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getItems()) .filter(entry -> entry.getJavaIdentifier().contains("arrow") && !entry.getJavaIdentifier().contains("spectral")) .collect(Collectors.toList()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java index 0d1538eb8..95dd07f22 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/BannerTranslator.java @@ -76,8 +76,7 @@ public class BannerTranslator extends NbtItemStackTranslator { } public BannerTranslator() { - appliedItems = Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) - .getItems()) + appliedItems = Arrays.stream(Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getItems()) .filter(entry -> entry.getJavaIdentifier().endsWith("banner")) .collect(Collectors.toList()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java index 1c9ded0c1..58b59d82e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java @@ -168,11 +168,12 @@ public class JavaMerchantOffersTranslator extends PacketTranslator