From 3236548a7a034a63bbf32ea5134d4f260faadcd6 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Thu, 10 Jul 2025 21:22:46 +0700 Subject: [PATCH 1/3] Initial work on server-sided fireworks boosting. --- .../populator/ItemRegistryPopulator.java | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) 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 545a8df0524..89be055096f 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 @@ -179,6 +179,8 @@ public static void populate() { CustomItemRegistryPopulator.populate(items, customItems, nonVanillaCustomItems); } + ItemDefinition oldFireworkDefinition = null; + // We can reduce some operations as Java information is the same across all palette versions boolean firstMappingsPass = true; @@ -218,6 +220,10 @@ public static void populate() { ItemDefinition definition = new SimpleItemDefinition(entry.getName().intern(), id, ItemVersion.from(entry.getVersion()), entry.isComponentBased(), components); definitions.put(entry.getName(), definition); registry.put(definition.getRuntimeId(), definition); + + if (definition.getIdentifier().contains("firework_rocket")) { + oldFireworkDefinition = definition; + } } Object2ObjectMap bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>(); @@ -578,14 +584,14 @@ public static void populate() { if (customItemsAllowed) { // Add furnace minecart int furnaceMinecartId = nextFreeBedrockId++; - ItemDefinition definition = new SimpleItemDefinition("geysermc:furnace_minecart", furnaceMinecartId, ItemVersion.DATA_DRIVEN, true, registerFurnaceMinecart(furnaceMinecartId)); - definitions.put("geysermc:furnace_minecart", definition); - registry.put(definition.getRuntimeId(), definition); + ItemDefinition furnaceMinecartDefinition = new SimpleItemDefinition("geysermc:furnace_minecart", furnaceMinecartId, ItemVersion.DATA_DRIVEN, true, registerFurnaceMinecart(furnaceMinecartId)); + definitions.put("geysermc:furnace_minecart", furnaceMinecartDefinition); + registry.put(furnaceMinecartDefinition.getRuntimeId(), furnaceMinecartDefinition); mappings.set(Items.FURNACE_MINECART.javaId(), ItemMapping.builder() .javaItem(Items.FURNACE_MINECART) .bedrockIdentifier("geysermc:furnace_minecart") - .bedrockDefinition(definition) + .bedrockDefinition(furnaceMinecartDefinition) .bedrockData(0) .bedrockBlockDefinition(null) .customItemOptions(Collections.emptyList()) // TODO check for custom items with furnace minecart @@ -594,10 +600,44 @@ public static void populate() { creativeItems.add(new CreativeItemData(ItemData.builder() .usingNetId(true) .netId(creativeNetId.incrementAndGet()) - .definition(definition) + .definition(furnaceMinecartDefinition) .count(1) .build(), creativeNetId.get(), 99)); // todo do not hardcode! + // Register a custom fireworks item to workaround client-sided boosting. + if (oldFireworkDefinition != null) { + int fireworkRocketId = nextFreeBedrockId++; + ItemDefinition fireworkRocketDefinition = new SimpleItemDefinition("geysermc:firework_rocket", fireworkRocketId, ItemVersion.DATA_DRIVEN, true, registerFireworkRocket(fireworkRocketId)); + // definitions.put("geysermc:firework_rocket", fireworkRocketDefinition); + registry.put(fireworkRocketDefinition.getRuntimeId(), fireworkRocketDefinition); + definitions.put(oldFireworkDefinition.getIdentifier(), fireworkRocketDefinition); + + mappings.set(Items.FIREWORK_ROCKET.javaId(), ItemMapping.builder() + .javaItem(Items.FIREWORK_ROCKET) + .bedrockIdentifier("geysermc:firework_rocket") + .bedrockDefinition(fireworkRocketDefinition) + .bedrockData(0) + .bedrockBlockDefinition(null) + .customItemOptions(Collections.emptyList()) // TODO check for custom items with furnace minecart + .build()); + + // We have to replace all the real firework rocket in the creative menu with our fake fireworks rocket. + for (int i = 0; i < creativeItems.size(); i++) { + CreativeItemData data = creativeItems.get(i); + if (data.getItem().getDefinition().getRuntimeId() != oldFireworkDefinition.getRuntimeId()) { + continue; + } + + creativeItems.set(i, new CreativeItemData(ItemData.builder() + .usingNetId(true) + .netId(data.getItem().getNetId()) + .definition(fireworkRocketDefinition) + .tag(data.getItem().getTag()) + .count(data.getItem().getCount()) + .build(), data.getNetId(), data.getGroupId())); + } + } + // Register any completely custom items given to us IntSet registeredJavaIds = new IntOpenHashSet(); // Used to check for duplicate item java ids for (NonVanillaCustomItemData customItem : nonVanillaCustomItems) { @@ -688,6 +728,33 @@ public static void populate() { } } + private static NbtMap registerFireworkRocket(int nextFreeBedrockId) { + NbtMapBuilder builder = NbtMap.builder(); + builder.putString("name", "geysermc:firework_rocket").putInt("id", nextFreeBedrockId); + + NbtMapBuilder itemProperties = NbtMap.builder(); + + NbtMapBuilder componentBuilder = NbtMap.builder(); + NbtMap iconMap = NbtMap.builder() + .putCompound("textures", NbtMap.builder() + .putString("default", "fireworks") + .build()) + .build(); + itemProperties.putCompound("minecraft:icon", iconMap); + componentBuilder.putCompound("minecraft:display_name", NbtMap.builder().putString("value", "item.fireworks.name").build()); + + // We always want to allow offhand usage when we can - matches Java Edition + itemProperties.putBoolean("allow_off_hand", true); + itemProperties.putBoolean("hand_equipped", false); + itemProperties.putInt("max_stack_size", 64); + itemProperties.putString("creative_group", "itemGroup.name.firework"); + itemProperties.putInt("creative_category", 4); // 4 - "Items" + + componentBuilder.putCompound("item_properties", itemProperties.build()); + builder.putCompound("components", componentBuilder.build()); + return builder.build(); + } + private static NbtMap registerFurnaceMinecart(int nextFreeBedrockId) { NbtMapBuilder builder = NbtMap.builder(); builder.putString("name", "geysermc:furnace_minecart") From 14d3dea7c848ed44a13ab37bc1c5eec36af36b89 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Thu, 10 Jul 2025 22:15:16 +0700 Subject: [PATCH 2/3] Remove this comment. --- .../geyser/registry/populator/ItemRegistryPopulator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 89be055096f..aa6dfcc28b8 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 @@ -618,7 +618,7 @@ public static void populate() { .bedrockDefinition(fireworkRocketDefinition) .bedrockData(0) .bedrockBlockDefinition(null) - .customItemOptions(Collections.emptyList()) // TODO check for custom items with furnace minecart + .customItemOptions(Collections.emptyList()) .build()); // We have to replace all the real firework rocket in the creative menu with our fake fireworks rocket. From 2faa36c9c967a6a2e0a1b0d86a165a82eb83d66a Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 11 Jul 2025 11:05:55 +0700 Subject: [PATCH 3/3] Translate the fireworks tag to lore. --- .../hashing/data/FireworkExplosionShape.java | 17 ++++-- .../geyser/item/type/FireworkRocketItem.java | 59 +++++++++++++++++-- .../geysermc/geyser/level/FireworkColor.java | 40 +++++++------ .../populator/ItemRegistryPopulator.java | 23 ++++++-- 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java index 316253e3e29..a0b76433879 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java @@ -25,11 +25,18 @@ package org.geysermc.geyser.item.hashing.data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + // Ordered and named by Java ID +@RequiredArgsConstructor +@Getter public enum FireworkExplosionShape { - SMALL_BALL, - LARGE_BALL, - STAR, - CREEPER, - BURST + SMALL_BALL("Small ball"), + LARGE_BALL("Large Ball"), + STAR("Star-shaped"), + CREEPER("Creeper-shaped"), + BURST("Burst"); + + private final String bedrockName; } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java index a0060044f01..87294591898 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java @@ -27,14 +27,17 @@ import it.unimi.dsi.fastutil.ints.IntArrays; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.item.TooltipOptions; +import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape; import org.geysermc.geyser.level.FireworkColor; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -49,13 +52,16 @@ public FireworkRocketItem(String javaIdentifier, Builder builder) { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, tooltip, builder); + public void translateComponentsToBedrock(@Nullable GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + if (session != null) { + super.translateComponentsToBedrock(session, components, tooltip, builder); + } Fireworks fireworks = components.get(DataComponentTypes.FIREWORKS); if (fireworks == null) { return; } + // We still need to translate the explosion so this is still correct, and can be reverse translate in translateNbtToJava. NbtMapBuilder fireworksNbt = NbtMap.builder(); fireworksNbt.putByte("Flight", (byte) fireworks.getFlightDuration()); @@ -71,14 +77,55 @@ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNul fireworksNbt.put("Explosions", NbtList.EMPTY); } builder.putCompound("Fireworks", fireworksNbt.build()); + + // Then we translate everything into lore since the explosion tag and everything is not visible anymore due to this being a custom item. + List lore = builder.getOrCreateLore(); + lore.add(ChatColor.RESET + ChatColor.WHITE + "Flight Duration: " + fireworks.getFlightDuration()); + + for (Fireworks.FireworkExplosion explosion : explosions) { + lore.add(ChatColor.RESET + ChatColor.WHITE + " " + FireworkExplosionShape.values()[explosion.getShapeId()].getBedrockName()); + + final StringBuilder colorBuilder = new StringBuilder(); + for (int color : explosion.getColors()) { + FireworkColor fireworkColor = FireworkColor.values()[FireworkColor.fromJavaRGB(color)]; + colorBuilder.append(fireworkColor.getBedrockName()).append(" "); + } + if (!colorBuilder.isEmpty()) { + lore.add(ChatColor.RESET + ChatColor.WHITE + " " + colorBuilder); + } + + final StringBuilder fadeColorBuilder = new StringBuilder(); + for (int color : explosion.getFadeColors()) { + FireworkColor fireworkColor = FireworkColor.values()[FireworkColor.fromJavaRGB(color)]; + fadeColorBuilder.append(fireworkColor.getBedrockName()).append(" "); + } + if (!fadeColorBuilder.isEmpty()) { + lore.add(ChatColor.RESET + ChatColor.WHITE + " Fade to " + fadeColorBuilder); + } + + if (explosion.isHasTrail()) { + lore.add(ChatColor.RESET + ChatColor.WHITE + " Trail"); + } + + if (explosion.isHasTwinkle()) { + lore.add(ChatColor.RESET + ChatColor.WHITE + " Twinkle"); + } + } } @Override - public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { - super.translateNbtToJava(session, bedrockTag, components, mapping); + public void translateNbtToJava(@Nullable GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + if (session != null) { + super.translateNbtToJava(session, bedrockTag, components, mapping); + } NbtMap fireworksTag = bedrockTag.getCompound("Fireworks"); if (!fireworksTag.isEmpty()) { + int flightDuration = 1; + if (fireworksTag.containsKey("Flight")) { + flightDuration = fireworksTag.getByte("Flight"); + } + List explosions = fireworksTag.getList("Explosions", NbtType.COMPOUND); if (!explosions.isEmpty()) { List javaExplosions = new ArrayList<>(); @@ -88,7 +135,9 @@ public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap b javaExplosions.add(javaExplosion); } } - components.put(DataComponentTypes.FIREWORKS, new Fireworks(1, javaExplosions)); + components.put(DataComponentTypes.FIREWORKS, new Fireworks(flightDuration, javaExplosions)); + } else { + components.put(DataComponentTypes.FIREWORKS, new Fireworks(flightDuration, List.of())); } } } diff --git a/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java b/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java index 2ee8509a0c1..c4f250621dd 100644 --- a/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java +++ b/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -25,34 +25,38 @@ package org.geysermc.geyser.level; +import lombok.Getter; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.util.HSVLike; import org.checkerframework.checker.nullness.qual.NonNull; public enum FireworkColor { - BLACK(1973019), - RED(11743532), - GREEN(3887386), - BROWN(5320730), - BLUE(2437522), - PURPLE(8073150), - CYAN(2651799), - LIGHT_GRAY(11250603), - GRAY(4408131), - PINK(14188952), - LIME(4312372), - YELLOW(14602026), - LIGHT_BLUE(6719955), - MAGENTA(12801229), - ORANGE(15435844), - WHITE(15790320); + BLACK(1973019, "Black"), + RED(11743532, "Red"), + GREEN(3887386, "Green"), + BROWN(5320730, "Brown"), + BLUE(2437522, "Blue"), + PURPLE(8073150, "Purple"), + CYAN(2651799, "Cyan"), + LIGHT_GRAY(11250603, "Silver"), + GRAY(4408131, "Gray"), + PINK(14188952, "Pink"), + LIME(4312372, "Lime"), + YELLOW(14602026, "Yellow"), + LIGHT_BLUE(6719955, "Light Blue"), + MAGENTA(12801229, "Magenta"), + ORANGE(15435844, "Orange"), + WHITE(15790320, "White"); private static final FireworkColor[] VALUES = values(); private final TextColor color; + @Getter + private final String bedrockName; - FireworkColor(int rgbValue) { + FireworkColor(int rgbValue, String bedrockName) { this.color = TextColor.color(rgbValue); + this.bedrockName = bedrockName; } private static HSVLike toHSV(int rgbValue) { 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 aa6dfcc28b8..4a3b99a0750 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 @@ -68,6 +68,7 @@ import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.block.property.Properties; @@ -80,6 +81,8 @@ import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.NonVanillaItemRegistration; import org.geysermc.geyser.registry.type.PaletteItem; +import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import java.io.InputStream; import java.util.ArrayList; @@ -608,18 +611,19 @@ public static void populate() { if (oldFireworkDefinition != null) { int fireworkRocketId = nextFreeBedrockId++; ItemDefinition fireworkRocketDefinition = new SimpleItemDefinition("geysermc:firework_rocket", fireworkRocketId, ItemVersion.DATA_DRIVEN, true, registerFireworkRocket(fireworkRocketId)); - // definitions.put("geysermc:firework_rocket", fireworkRocketDefinition); registry.put(fireworkRocketDefinition.getRuntimeId(), fireworkRocketDefinition); definitions.put(oldFireworkDefinition.getIdentifier(), fireworkRocketDefinition); - mappings.set(Items.FIREWORK_ROCKET.javaId(), ItemMapping.builder() + ItemMapping mapping = ItemMapping.builder() .javaItem(Items.FIREWORK_ROCKET) .bedrockIdentifier("geysermc:firework_rocket") .bedrockDefinition(fireworkRocketDefinition) .bedrockData(0) .bedrockBlockDefinition(null) .customItemOptions(Collections.emptyList()) - .build()); + .build(); + + mappings.set(Items.FIREWORK_ROCKET.javaId(), mapping); // We have to replace all the real firework rocket in the creative menu with our fake fireworks rocket. for (int i = 0; i < creativeItems.size(); i++) { @@ -628,11 +632,22 @@ public static void populate() { continue; } + // Since the fireworks tag is now useless due to this is being a custom item, we have to translate it to lore ourselves. + NbtMap tag = null; + if (data.getItem().getTag() != null) { + final DataComponents components = new DataComponents(new HashMap<>()); + Items.FIREWORK_ROCKET.translateNbtToJava(null, data.getItem().getTag(), components, mapping); + final BedrockItemBuilder builder = new BedrockItemBuilder(); + Items.FIREWORK_ROCKET.translateComponentsToBedrock(null, components, TooltipOptions.ALL_SHOWN, builder); + + tag = builder.build(); + } + creativeItems.set(i, new CreativeItemData(ItemData.builder() .usingNetId(true) .netId(data.getItem().getNetId()) .definition(fireworkRocketDefinition) - .tag(data.getItem().getTag()) + .tag(tag) .count(data.getItem().getCount()) .build(), data.getNetId(), data.getGroupId())); }