From c8e9f02a9fea6921d81a67f3e9f17d4e49921f3f Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:41:56 -0600 Subject: [PATCH 01/13] server: Introduced missing vanilla item component types. --- server/internal/iteminternal/components.go | 250 ++++++++++++++++++++- server/item/camera.go | 26 +++ server/item/enchantable_data.go | 17 ++ server/item/fire_resistant.go | 7 + server/item/food.go | 47 ++++ server/item/item.go | 12 + server/item/repair.go | 20 ++ server/item/seed.go | 21 ++ server/item/storage.go | 27 +++ server/item/swing.go | 23 ++ server/item/tags.go | 8 + server/item/use_modifiers.go | 23 ++ server/item/weapon.go | 59 +++++ 13 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 server/item/camera.go create mode 100644 server/item/enchantable_data.go create mode 100644 server/item/fire_resistant.go create mode 100644 server/item/food.go create mode 100644 server/item/repair.go create mode 100644 server/item/seed.go create mode 100644 server/item/storage.go create mode 100644 server/item/swing.go create mode 100644 server/item/tags.go create mode 100644 server/item/use_modifiers.go create mode 100644 server/item/weapon.go diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index ed6f64d1ef..e49de74444 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -1,9 +1,10 @@ package iteminternal import ( + "strings" + "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" - "strings" ) // Components returns all the components of the given custom item. If the item has no components, a nil map and false @@ -36,11 +37,50 @@ func Components(it world.CustomItem) map[string]any { }) } if x, ok := it.(item.Consumable); ok { - builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) - builder.AddComponent("minecraft:food", map[string]any{ + food := map[string]any{ "can_always_eat": x.AlwaysConsumable(), - }) + } + if y, ok := it.(item.Food); ok { + info := y.FoodInfo() + food["nutrition"] = int32(info.Nutrition) + food["saturation_modifier"] = float32(info.SaturationModifier) + if info.UsingConvertsTo != "" { + food["using_converts_to"] = info.UsingConvertsTo + } + if info.OnUseAction != 0 { + food["on_use_action"] = int32(info.OnUseAction) + } + if info.CooldownTime != 0 { + food["cooldown_time"] = int32(info.CooldownTime) + food["cooldown_type"] = info.CooldownType + } + if len(info.Effects) != 0 { + effects := make([]any, 0, len(info.Effects)) + for _, e := range info.Effects { + m := map[string]any{ + "id": int32(e.ID), + "duration": int32(e.Duration), + "amplifier": int32(e.Amplifier), + "chance": float32(e.Chance), + } + if e.Name != "" { + m["name"] = e.Name + } + effects = append(effects, m) + } + food["effects"] = effects + } + if len(info.RemoveEffects) != 0 { + removeEffects := make([]any, 0, len(info.RemoveEffects)) + for _, id := range info.RemoveEffects { + removeEffects = append(removeEffects, int32(id)) + } + food["remove_effects"] = removeEffects + } + } + builder.AddComponent("minecraft:food", food) + builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) if y, ok := it.(item.Drinkable); ok && y.Drinkable() { builder.AddProperty("use_animation", int32(2)) } else { @@ -48,10 +88,14 @@ func Components(it world.CustomItem) map[string]any { } } if x, ok := it.(item.Cooldown); ok { - builder.AddComponent("minecraft:cooldown", map[string]any{ + cooldown := map[string]any{ "category": name, "duration": float32(x.Cooldown().Seconds()), - }) + } + if y, ok := it.(item.CooldownTyped); ok { + cooldown["type"] = y.CooldownType() + } + builder.AddComponent("minecraft:cooldown", cooldown) } if x, ok := it.(item.Durable); ok { builder.AddComponent("minecraft:durability", map[string]any{ @@ -64,6 +108,9 @@ func Components(it world.CustomItem) map[string]any { if x, ok := it.(item.OffHand); ok { builder.AddProperty("allow_off_hand", x.OffHand()) } + if x, ok := it.(item.StackedByData); ok { + builder.AddProperty("stacked_by_data", x.StackedByData()) + } if x, ok := it.(item.Throwable); ok { // The data in minecraft:projectile is only used by vanilla server-side, but we must send at least an empty map // so the client will play the throwing animation. @@ -78,5 +125,196 @@ func Components(it world.CustomItem) map[string]any { if x, ok := it.(item.HandEquipped); ok { builder.AddProperty("hand_equipped", x.HandEquipped()) } + if x, ok := it.(item.Weapon); ok { + builder.AddComponent("minecraft:damage", map[string]any{ + "value": int32(x.AttackDamage()), + }) + } + if x, ok := it.(item.Fuel); ok { + builder.AddComponent("minecraft:fuel", map[string]any{ + "duration": float32(x.FuelInfo().Duration.Seconds()), + }) + } + if x, ok := it.(item.FireResistant); ok { + builder.AddComponent("minecraft:fire_resistant", map[string]any{ + "value": x.FireResistant(), + }) + } + if x, ok := it.(item.EnchantableData); ok { + info := x.EnchantableData() + builder.AddComponent("minecraft:enchantable", map[string]any{ + "slot": info.Slot, + "value": int32(info.Value), + }) + } + if x, ok := it.(item.RepairMaterials); ok { + builder.AddComponent("minecraft:repairable", map[string]any{ + "repair_items": repairItems(x.RepairMaterials()), + }) + } + if x, ok := it.(item.Tagged); ok { + tags := stringSlice(x.Tags()) + builder.AddComponent("item_tags", tags) + builder.AddComponent("minecraft:tags", map[string]any{"tags": tags}) + } + if x, ok := it.(item.Seed); ok { + info := x.SeedInfo() + builder.AddComponent("minecraft:seed", map[string]any{ + "crop_result": info.CropResult, + "plant_at": stringSlice(info.PlantAt), + "plant_at_any_solid_surface": info.PlantAtAnySolidSurface, + "plant_at_face": info.PlantAtFace, + }) + } + if x, ok := it.(item.Storage); ok { + info := x.StorageInfo() + if info.NumViewableSlots != 0 { + builder.AddComponent("minecraft:bundle_interaction", map[string]any{ + "num_viewable_slots": int32(info.NumViewableSlots), + }) + } + if info.MaxSlots != 0 || len(info.AllowedItems) != 0 || len(info.BannedItems) != 0 { + builder.AddComponent("minecraft:storage_item", map[string]any{ + "allow_nested_storage_items": info.AllowNestedStorageItems, + "allowed_items": stringSlice(info.AllowedItems), + "banned_items": bannedItems(info.BannedItems), + "max_slots": int32(info.MaxSlots), + }) + } + if info.MaxWeightLimit != 0 { + builder.AddComponent("minecraft:storage_weight_limit", map[string]any{ + "max_weight_limit": int32(info.MaxWeightLimit), + }) + } + if info.WeightInStorageItem != 0 { + builder.AddComponent("minecraft:storage_weight_modifier", map[string]any{ + "weight_in_storage_item": int32(info.WeightInStorageItem), + }) + } + } + if x, ok := it.(item.UseModifiers); ok { + info := x.UseModifiers() + modifiers := map[string]any{ + "emit_vibrations": info.EmitVibrations, + "movement_modifier": float32(info.MovementModifier), + "use_duration": float32(info.UseDuration), + } + if info.StartSound != "" { + modifiers["start_sound"] = info.StartSound + } + if info.StartUsing != "" { + modifiers["start_using"] = info.StartUsing + } + builder.AddComponent("minecraft:use_modifiers", modifiers) + } + if x, ok := it.(item.SwingDuration); ok { + builder.AddComponent("minecraft:swing_duration", map[string]any{ + "value": float32(x.SwingDuration()), + }) + } + if x, ok := it.(item.SwingSounds); ok { + info := x.SwingSounds() + builder.AddComponent("minecraft:swing_sounds", map[string]any{ + "attack_hit": info.AttackHit, + "attack_miss": info.AttackMiss, + }) + } + if x, ok := it.(item.KineticWeapon); ok { + builder.AddComponent("minecraft:kinetic_weapon", map[string]any{ + "minecraft:kinetic_weapon": kineticWeaponData(x.KineticWeaponInfo()), + }) + } + if x, ok := it.(item.PiercingWeapon); ok { + info := x.PiercingWeaponInfo() + builder.AddComponent("minecraft:piercing_weapon", map[string]any{ + "creative_reach": rangeData(info.CreativeReach), + "hitbox_margin": float32(info.HitboxMargin), + "reach": rangeData(info.Reach), + }) + } + if x, ok := it.(item.Camera); ok { + info := x.CameraInfo() + builder.AddComponent("minecraft:camera", map[string]any{ + "black_bars_duration": float32(info.BlackBarsDuration), + "black_bars_screen_ratio": float32(info.BlackBarsScreenRatio), + "picture_duration": float32(info.PictureDuration), + "shutter_duration": float32(info.ShutterDuration), + "shutter_screen_ratio": float32(info.ShutterScreenRatio), + "slide_away_duration": float32(info.SlideAwayDuration), + }) + builder.AddComponent("minecraft:block", "minecraft:camera") + if info.UseDuration != 0 { + builder.AddProperty("use_duration", int32(info.UseDuration)) + } + } return builder.Construct() } + +// repairItems converts the repair materials of an item to the data required for the minecraft:repairable +// component. +func repairItems(items []item.RepairItem) []any { + materials := make([]any, 0, len(items)) + for _, r := range items { + var entry []any + if r.Item != "" { + entry = []any{map[string]any{"name": r.Item}} + } else { + entry = []any{map[string]any{"tags": r.Tag}} + } + materials = append(materials, map[string]any{ + "items": entry, + "repair_amount": r.RepairAmount, + }) + } + return materials +} + +// bannedItems converts the banned item identifiers of a storage item to the data required for the +// minecraft:storage_item component. +func bannedItems(items []string) []any { + banned := make([]any, 0, len(items)) + for _, b := range items { + banned = append(banned, map[string]any{"name": b}) + } + return banned +} + +// rangeData converts a min/max range to the data required for the client. +func rangeData(r [2]float64) map[string]any { + return map[string]any{ + "min": float32(r[0]), + "max": float32(r[1]), + } +} + +// kineticWeaponData converts the kinetic weapon information of an item to the data required for the +// minecraft:kinetic_weapon component. +func kineticWeaponData(info item.KineticWeaponInfo) map[string]any { + conditions := func(c item.WeaponConditions) map[string]any { + return map[string]any{ + "max_duration": int32(c.MaxDuration), + "min_relative_speed": float32(c.MinRelativeSpeed), + "min_speed": float32(c.MinSpeed), + } + } + return map[string]any{ + "creative_reach": rangeData(info.CreativeReach), + "damage_conditions": conditions(info.DamageConditions), + "damage_modifier": float32(info.DamageModifier), + "damage_multiplier": float32(info.DamageMultiplier), + "delay": int32(info.Delay), + "dismount_conditions": conditions(info.DismountConditions), + "hitbox_margin": float32(info.HitboxMargin), + "knockback_conditions": conditions(info.KnockbackConditions), + "reach": rangeData(info.Reach), + } +} + +// stringSlice converts a slice of strings to a slice of any. +func stringSlice(x []string) []any { + s := make([]any, len(x)) + for i, v := range x { + s[i] = v + } + return s +} diff --git a/server/item/camera.go b/server/item/camera.go new file mode 100644 index 0000000000..7258b06bba --- /dev/null +++ b/server/item/camera.go @@ -0,0 +1,26 @@ +package item + +// Camera represents an item that behaves as a camera, allowing the user to take pictures. +type Camera interface { + // CameraInfo returns the camera information of the item. + CameraInfo() CameraInfo +} + +// CameraInfo is a struct returned by items that implement Camera. It contains the information required for +// the client to handle the item as a camera. +type CameraInfo struct { + // BlackBarsDuration is the duration in seconds the black bars are shown when taking a picture. + BlackBarsDuration float64 + // BlackBarsScreenRatio is the ratio of the screen covered by the black bars. + BlackBarsScreenRatio float64 + // PictureDuration is the duration in seconds the picture is shown. + PictureDuration float64 + // ShutterDuration is the duration in seconds the shutter is shown. + ShutterDuration float64 + // ShutterScreenRatio is the ratio of the screen covered by the shutter. + ShutterScreenRatio float64 + // SlideAwayDuration is the duration in seconds the picture slides away. + SlideAwayDuration float64 + // UseDuration is the duration in ticks the item takes to be fully used. + UseDuration int +} diff --git a/server/item/enchantable_data.go b/server/item/enchantable_data.go new file mode 100644 index 0000000000..336b18605d --- /dev/null +++ b/server/item/enchantable_data.go @@ -0,0 +1,17 @@ +package item + +// EnchantableData represents an item that can be enchanted and provides the data required for the client to +// display the enchantability of the item. +type EnchantableData interface { + // EnchantableData returns the enchantable data of the item. + EnchantableData() EnchantableInfo +} + +// EnchantableInfo is a struct returned by items that implement EnchantableData. It contains the information +// required for the client to display the enchantability of the item. +type EnchantableInfo struct { + // Slot is the enchantment slot of the item, such as "melee_spear" or "sword". + Slot string + // Value is the enchantment value of the item. + Value int +} diff --git a/server/item/fire_resistant.go b/server/item/fire_resistant.go new file mode 100644 index 0000000000..48cc6c19bf --- /dev/null +++ b/server/item/fire_resistant.go @@ -0,0 +1,7 @@ +package item + +// FireResistant represents an item that is resistant to fire and lava, such as netherite items. +type FireResistant interface { + // FireResistant returns whether the item is resistant to fire and lava. + FireResistant() bool +} diff --git a/server/item/food.go b/server/item/food.go new file mode 100644 index 0000000000..8d3d8711ca --- /dev/null +++ b/server/item/food.go @@ -0,0 +1,47 @@ +package item + +// Food represents an item that has nutritional value and provides the data required for the client to +// display the nutritional value of the item. +type Food interface { + // FoodInfo returns the food information of the item. + FoodInfo() FoodInfo +} + +// FoodInfo is a struct returned by items that implement Food. It contains the information required for the +// client to display the nutritional value of the item. +type FoodInfo struct { + // Nutrition is the number of hunger points the item restores. + Nutrition int + // SaturationModifier is the modifier applied to the saturation restored by the item. + SaturationModifier float64 + // UsingConvertsTo is the identifier of the item the item converts to when consumed, such as a bowl or + // glass bottle. + UsingConvertsTo string + // OnUseAction is the action performed when the item is used, such as the chorus fruit teleport. + OnUseAction int + // OnUseRange is the range in blocks the on use action applies to. + OnUseRange [3]float64 + // CooldownTime is the duration in seconds of the cooldown applied when the item is consumed. + CooldownTime int + // CooldownType is the type of cooldown applied when the item is consumed. + CooldownType string + // Effects is a list of effects applied to the consumer when the item is consumed. + Effects []FoodEffect + // RemoveEffects is a list of effect IDs removed from the consumer when the item is consumed. + RemoveEffects []int +} + +// FoodEffect is a struct returned by items that implement Food. It contains the information required for the +// client to display an effect applied when the item is consumed. +type FoodEffect struct { + // ID is the ID of the effect. + ID int + // Duration is the duration in seconds of the effect. + Duration int + // Amplifier is the amplifier of the effect. + Amplifier int + // Chance is the chance the effect is applied. + Chance float64 + // Name is the name of the effect, such as "hunger" or "poison". + Name string +} diff --git a/server/item/item.go b/server/item/item.go index 33a503cd2a..dc8d4a9c1f 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -137,6 +137,18 @@ type Cooldown interface { Cooldown() time.Duration } +// CooldownTyped represents an item that has a typed cooldown, such as an attack or use cooldown. +type CooldownTyped interface { + // CooldownType returns the type of cooldown of the item, such as "attack" or "use". + CooldownType() string +} + +// StackedByData represents an item that is stacked by its metadata value, such as fish or golden apples. +type StackedByData interface { + // StackedByData returns whether the item is stacked by its metadata value. + StackedByData() bool +} + // nameable represents a block that may be named. These are often containers such as chests, which have a // name displayed in their interface. type nameable interface { diff --git a/server/item/repair.go b/server/item/repair.go new file mode 100644 index 0000000000..f36d7150db --- /dev/null +++ b/server/item/repair.go @@ -0,0 +1,20 @@ +package item + +// RepairMaterials represents a durable item that can be repaired by other items and provides the data +// required for the client to display the repair materials of the item. +type RepairMaterials interface { + // RepairMaterials returns the repair materials of the item. + RepairMaterials() []RepairItem +} + +// RepairItem is a struct returned by items that implement RepairMaterials. It contains the information +// required for the client to display a single repair material of the item. +type RepairItem struct { + // Item is the identifier of the item used to repair the item. + Item string + // Tag is the tag used to match items that can repair the item. + Tag string + // RepairAmount is the expression used to determine the amount of durability restored when repairing the + // item. + RepairAmount string +} diff --git a/server/item/seed.go b/server/item/seed.go new file mode 100644 index 0000000000..98470f98d1 --- /dev/null +++ b/server/item/seed.go @@ -0,0 +1,21 @@ +package item + +// Seed represents an item that can be planted on blocks to grow a crop. +type Seed interface { + // SeedInfo returns the information of the item related to planting it as a seed. + SeedInfo() SeedInfo +} + +// SeedInfo is a struct returned by items that implement Seed. It contains the information required for the +// client to allow planting the item on the specified blocks. +type SeedInfo struct { + // CropResult is the identifier of the crop that grows when the seed is planted. + CropResult string + // PlantAt is a list of block identifiers that the seed may be planted on. This list is ignored if + // PlantAtAnySolidSurface is true. + PlantAt []string + // PlantAtAnySolidSurface is true if the seed may be planted on any solid surface. + PlantAtAnySolidSurface bool + // PlantAtFace is the face the seed is planted on, such as "up" or "down". + PlantAtFace string +} diff --git a/server/item/storage.go b/server/item/storage.go new file mode 100644 index 0000000000..b543598ec8 --- /dev/null +++ b/server/item/storage.go @@ -0,0 +1,27 @@ +package item + +// Storage represents an item that can store other items, such as a bundle or shulker box. +type Storage interface { + // StorageInfo returns the information of the item related to storing other items inside it. + StorageInfo() StorageInfo +} + +// StorageInfo is a struct returned by items that implement Storage. It contains the information required for +// the client to allow storing other items inside the item. +type StorageInfo struct { + // MaxSlots is the maximum number of slots the item may store items in. + MaxSlots int + // MaxWeightLimit is the maximum weight of items the item may store. + MaxWeightLimit int + // WeightInStorageItem is the weight of the item itself when stored inside another storage item. + WeightInStorageItem int + // NumViewableSlots is the number of slots that may be viewed when interacting with the item. If set to + // zero, no bundle interaction component is sent. + NumViewableSlots int + // AllowNestedStorageItems is true if other storage items may be stored inside the item. + AllowNestedStorageItems bool + // AllowedItems is a list of item identifiers that may be stored inside the item. + AllowedItems []string + // BannedItems is a list of item identifiers that may not be stored inside the item. + BannedItems []string +} diff --git a/server/item/swing.go b/server/item/swing.go new file mode 100644 index 0000000000..2a6250a518 --- /dev/null +++ b/server/item/swing.go @@ -0,0 +1,23 @@ +package item + +// SwingDuration represents an item with a custom duration in seconds of the swing animation played when the +// item is used to attack. +type SwingDuration interface { + // SwingDuration returns the duration in seconds of the swing animation. + SwingDuration() float64 +} + +// SwingSounds represents an item with custom sounds played when the item is swung. +type SwingSounds interface { + // SwingSounds returns the sounds played when the item is swung. + SwingSounds() SwingSoundsInfo +} + +// SwingSoundsInfo is a struct returned by items that implement SwingSounds. It contains the sounds played +// when the item is swung. +type SwingSoundsInfo struct { + // AttackHit is the sound played when an attack made with the item hits. + AttackHit string + // AttackMiss is the sound played when an attack made with the item misses. + AttackMiss string +} diff --git a/server/item/tags.go b/server/item/tags.go new file mode 100644 index 0000000000..e468e7a201 --- /dev/null +++ b/server/item/tags.go @@ -0,0 +1,8 @@ +package item + +// Tagged represents an item that has one or more item tags. These tags may be used by the client for various +// purposes, such as determining the tier of an item or checking if an item is food. +type Tagged interface { + // Tags returns the tags of the item. + Tags() []string +} diff --git a/server/item/use_modifiers.go b/server/item/use_modifiers.go new file mode 100644 index 0000000000..a35ce7e27f --- /dev/null +++ b/server/item/use_modifiers.go @@ -0,0 +1,23 @@ +package item + +// UseModifiers represents an item that modifies the way it is used, such as slowing down the user while it is +// being used. +type UseModifiers interface { + // UseModifiers returns the use modifiers of the item. + UseModifiers() UseModifiersInfo +} + +// UseModifiersInfo is a struct returned by items that implement UseModifiers. It contains the information +// required for the client to modify the way the item is used. +type UseModifiersInfo struct { + // MovementModifier is the modifier applied to the movement speed of the user while using the item. + MovementModifier float64 + // UseDuration is the duration in seconds the item takes to be fully used. + UseDuration float64 + // EmitVibrations is true if using the item emits vibrations. + EmitVibrations bool + // StartSound is the sound played when the item starts being used. + StartSound string + // StartUsing is the condition required to start using the item, such as "always" or "require_charging". + StartUsing string +} diff --git a/server/item/weapon.go b/server/item/weapon.go new file mode 100644 index 0000000000..58e5b37834 --- /dev/null +++ b/server/item/weapon.go @@ -0,0 +1,59 @@ +package item + +// KineticWeapon represents a weapon that deals damage based on the kinetic energy of the user's movement, +// such as a spear. +type KineticWeapon interface { + // KineticWeaponInfo returns the kinetic weapon information of the item. + KineticWeaponInfo() KineticWeaponInfo +} + +// KineticWeaponInfo is a struct returned by items that implement KineticWeapon. It contains the information +// required for the client to handle the item as a kinetic weapon. +type KineticWeaponInfo struct { + // Reach is the minimum and maximum reach of the weapon. + Reach [2]float64 + // CreativeReach is the minimum and maximum reach of the weapon in creative mode. + CreativeReach [2]float64 + // HitboxMargin is the margin of the hitbox of the weapon. + HitboxMargin float64 + // DamageMultiplier is the multiplier applied to the damage dealt by the weapon. + DamageMultiplier float64 + // DamageModifier is the modifier applied to the damage dealt by the weapon. + DamageModifier float64 + // Delay is the delay in ticks before the weapon can be used again. + Delay int + // DamageConditions are the conditions required for the weapon to deal its full damage. + DamageConditions WeaponConditions + // KnockbackConditions are the conditions required for the weapon to knock back its target. + KnockbackConditions WeaponConditions + // DismountConditions are the conditions required for the weapon to dismount its target. + DismountConditions WeaponConditions +} + +// WeaponConditions is a struct returned by items that implement KineticWeapon. It contains the conditions +// required for the weapon to apply certain effects. +type WeaponConditions struct { + // MaxDuration is the maximum duration in ticks for which the condition applies. + MaxDuration int + // MinSpeed is the minimum speed of the user for which the condition applies. + MinSpeed float64 + // MinRelativeSpeed is the minimum relative speed of the user for which the condition applies. + MinRelativeSpeed float64 +} + +// PiercingWeapon represents a weapon that can pierce through targets, such as a spear. +type PiercingWeapon interface { + // PiercingWeaponInfo returns the piercing weapon information of the item. + PiercingWeaponInfo() PiercingWeaponInfo +} + +// PiercingWeaponInfo is a struct returned by items that implement PiercingWeapon. It contains the information +// required for the client to handle the item as a piercing weapon. +type PiercingWeaponInfo struct { + // Reach is the minimum and maximum reach of the weapon. + Reach [2]float64 + // CreativeReach is the minimum and maximum reach of the weapon in creative mode. + CreativeReach [2]float64 + // HitboxMargin is the margin of the hitbox of the weapon. + HitboxMargin float64 +} From cd487d7aa3fd323e2c31574145c5b66b301ed359 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:08:08 -0600 Subject: [PATCH 02/13] server: Expanded added components. --- server/internal/iteminternal/components.go | 16 +++++++++++----- server/item/item.go | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index e49de74444..2961fda9dc 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -17,10 +17,6 @@ func Components(it world.CustomItem) map[string]any { builder := NewComponentBuilder(it.Name(), identifier, category) if x, ok := it.(item.Armour); ok { - builder.AddComponent("minecraft:armor", map[string]any{ - "protection": int32(x.DefencePoints()), - }) - var slot string switch it.(type) { case item.HelmetType: @@ -33,7 +29,8 @@ func Components(it world.CustomItem) map[string]any { slot = "slot.armor.feet" } builder.AddComponent("minecraft:wearable", map[string]any{ - "slot": slot, + "slot": slot, + "protection": int32(x.DefencePoints()), }) } if x, ok := it.(item.Consumable); ok { @@ -111,6 +108,15 @@ func Components(it world.CustomItem) map[string]any { if x, ok := it.(item.StackedByData); ok { builder.AddProperty("stacked_by_data", x.StackedByData()) } + if x, ok := it.(item.MiningSpeed); ok { + builder.AddProperty("mining_speed", float32(x.MiningSpeed())) + } + if x, ok := it.(item.FrameCount); ok { + builder.AddProperty("frame_count", int32(x.FrameCount())) + } + if x, ok := it.(item.CanDestroyInCreative); ok { + builder.AddProperty("can_destroy_in_creative", x.CanDestroyInCreative()) + } if x, ok := it.(item.Throwable); ok { // The data in minecraft:projectile is only used by vanilla server-side, but we must send at least an empty map // so the client will play the throwing animation. diff --git a/server/item/item.go b/server/item/item.go index dc8d4a9c1f..8736cc78c5 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -149,6 +149,24 @@ type StackedByData interface { StackedByData() bool } +// MiningSpeed represents an item with a custom mining speed. +type MiningSpeed interface { + // MiningSpeed returns the mining speed of the item. + MiningSpeed() float64 +} + +// FrameCount represents an item with a custom amount of animation frames in its icon texture. +type FrameCount interface { + // FrameCount returns the amount of animation frames in the icon texture of the item. + FrameCount() int +} + +// CanDestroyInCreative represents an item that can be used to destroy blocks in creative mode. +type CanDestroyInCreative interface { + // CanDestroyInCreative returns whether the item can be used to destroy blocks in creative mode. + CanDestroyInCreative() bool +} + // nameable represents a block that may be named. These are often containers such as chests, which have a // name displayed in their interface. type nameable interface { From dfb9e8c0f17bcd476de5c4ec66fda389f18462ad Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:57:49 -0600 Subject: [PATCH 03/13] Extended components based off MSDocs. --- main.go | 10 +- server/internal/iteminternal/builder.go | 5 +- server/internal/iteminternal/components.go | 199 +++++++++++++++++---- server/item/armour.go | 4 + server/item/block_placer.go | 22 +++ server/item/damage_absorption.go | 8 + server/item/digger.go | 26 +++ server/item/durability_sensor.go | 29 +++ server/item/dyeable.go | 7 + server/item/enchantable_data.go | 2 +- server/item/entity_placer.go | 20 +++ server/item/hover_text_color.go | 7 + server/item/interact_button.go | 8 + server/item/liquid_clipped.go | 7 + server/item/rarity.go | 9 + server/item/record.go | 18 ++ server/item/storage.go | 4 +- server/server.go | 25 ++- 18 files changed, 358 insertions(+), 52 deletions(-) create mode 100644 server/item/block_placer.go create mode 100644 server/item/damage_absorption.go create mode 100644 server/item/digger.go create mode 100644 server/item/durability_sensor.go create mode 100644 server/item/dyeable.go create mode 100644 server/item/entity_placer.go create mode 100644 server/item/hover_text_color.go create mode 100644 server/item/interact_button.go create mode 100644 server/item/liquid_clipped.go create mode 100644 server/item/rarity.go create mode 100644 server/item/record.go diff --git a/main.go b/main.go index 03c5def921..8c5d988848 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,12 @@ package main import ( "fmt" + "log/slog" + "os" + "github.com/df-mc/dragonfly/server" "github.com/df-mc/dragonfly/server/player/chat" "github.com/pelletier/go-toml" - "log/slog" - "os" ) func main() { @@ -20,7 +21,10 @@ func main() { srv := conf.New() srv.CloseOnProgramEnd() - srv.Listen() + if err := srv.Listen(); err != nil { + panic(err) + } + for p := range srv.Accept() { _ = p } diff --git a/server/internal/iteminternal/builder.go b/server/internal/iteminternal/builder.go index afffef0741..5690d3b648 100644 --- a/server/internal/iteminternal/builder.go +++ b/server/internal/iteminternal/builder.go @@ -1,8 +1,9 @@ package iteminternal import ( - "github.com/df-mc/dragonfly/server/item/category" "maps" + + "github.com/df-mc/dragonfly/server/item/category" ) // ComponentBuilder represents a builder that can be used to construct an item components map to be sent to a client. @@ -65,7 +66,7 @@ func (builder *ComponentBuilder) applyDefaultProperties(x map[string]any) { // applyDefaultComponents applies the default components to the provided map. It is important that this method does not // modify the builder's components map directly otherwise Empty() will return false in future use of the builder. func (builder *ComponentBuilder) applyDefaultComponents(x, properties map[string]any) { - x["item_properties"] = properties + x["components"] = properties x["minecraft:display_name"] = map[string]any{ "value": builder.name, } diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index 2961fda9dc..b2a0bfb2c6 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -1,6 +1,7 @@ package iteminternal import ( + "fmt" "strings" "github.com/df-mc/dragonfly/server/item" @@ -9,10 +10,15 @@ import ( // Components returns all the components of the given custom item. If the item has no components, a nil map and false // are returned. -func Components(it world.CustomItem) map[string]any { +func Components(it world.CustomItem) (map[string]any, error) { category := it.Category() identifier, _ := it.EncodeItem() - name := strings.Split(identifier, ":")[1] + + parts := strings.SplitN(identifier, ":", 1) + if len(parts) < 2 { + return nil, fmt.Errorf("indetifier %s must contain namespace.", identifier) + } + name := parts[1] builder := NewComponentBuilder(it.Name(), identifier, category) @@ -28,9 +34,10 @@ func Components(it world.CustomItem) map[string]any { case item.BootsType: slot = "slot.armor.feet" } - builder.AddComponent("minecraft:wearable", map[string]any{ - "slot": slot, - "protection": int32(x.DefencePoints()), + builder.AddComponent("wearable", map[string]any{ + "slot": slot, + "protection": int32(x.DefencePoints()), + "hides_player_location": x.HidesPlayerLocation(), }) } if x, ok := it.(item.Consumable); ok { @@ -75,7 +82,7 @@ func Components(it world.CustomItem) map[string]any { food["remove_effects"] = removeEffects } } - builder.AddComponent("minecraft:food", food) + builder.AddComponent("food", food) builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) if y, ok := it.(item.Drinkable); ok && y.Drinkable() { @@ -92,10 +99,10 @@ func Components(it world.CustomItem) map[string]any { if y, ok := it.(item.CooldownTyped); ok { cooldown["type"] = y.CooldownType() } - builder.AddComponent("minecraft:cooldown", cooldown) + builder.AddComponent("cooldown", cooldown) } if x, ok := it.(item.Durable); ok { - builder.AddComponent("minecraft:durability", map[string]any{ + builder.AddComponent("durability", map[string]any{ "max_durability": int32(x.DurabilityInfo().MaxDurability), }) } @@ -118,54 +125,54 @@ func Components(it world.CustomItem) map[string]any { builder.AddProperty("can_destroy_in_creative", x.CanDestroyInCreative()) } if x, ok := it.(item.Throwable); ok { - // The data in minecraft:projectile is only used by vanilla server-side, but we must send at least an empty map - // so the client will play the throwing animation. - builder.AddComponent("minecraft:projectile", map[string]any{}) - builder.AddComponent("minecraft:throwable", map[string]any{ + builder.AddComponent("projectile", map[string]any{}) + builder.AddComponent("throwable", map[string]any{ "do_swing_animation": x.SwingAnimation(), }) } if x, ok := it.(item.Glinted); ok { - builder.AddProperty("foil", x.Glinted()) + builder.AddComponent("glint", map[string]any{ + "value": x.Glinted(), + }) } if x, ok := it.(item.HandEquipped); ok { builder.AddProperty("hand_equipped", x.HandEquipped()) } if x, ok := it.(item.Weapon); ok { - builder.AddComponent("minecraft:damage", map[string]any{ - "value": int32(x.AttackDamage()), + builder.AddComponent("damage", map[string]any{ + "value": x.AttackDamage(), }) } if x, ok := it.(item.Fuel); ok { - builder.AddComponent("minecraft:fuel", map[string]any{ + builder.AddComponent("fuel", map[string]any{ "duration": float32(x.FuelInfo().Duration.Seconds()), }) } if x, ok := it.(item.FireResistant); ok { - builder.AddComponent("minecraft:fire_resistant", map[string]any{ + builder.AddComponent("fire_resistant", map[string]any{ "value": x.FireResistant(), }) } if x, ok := it.(item.EnchantableData); ok { info := x.EnchantableData() - builder.AddComponent("minecraft:enchantable", map[string]any{ + builder.AddComponent("enchantable", map[string]any{ "slot": info.Slot, - "value": int32(info.Value), + "value": info.Value, }) } if x, ok := it.(item.RepairMaterials); ok { - builder.AddComponent("minecraft:repairable", map[string]any{ + builder.AddComponent("repairable", map[string]any{ "repair_items": repairItems(x.RepairMaterials()), }) } if x, ok := it.(item.Tagged); ok { tags := stringSlice(x.Tags()) builder.AddComponent("item_tags", tags) - builder.AddComponent("minecraft:tags", map[string]any{"tags": tags}) + builder.AddComponent("tags", map[string]any{"tags": tags}) } if x, ok := it.(item.Seed); ok { info := x.SeedInfo() - builder.AddComponent("minecraft:seed", map[string]any{ + builder.AddComponent("seed", map[string]any{ "crop_result": info.CropResult, "plant_at": stringSlice(info.PlantAt), "plant_at_any_solid_surface": info.PlantAtAnySolidSurface, @@ -175,12 +182,15 @@ func Components(it world.CustomItem) map[string]any { if x, ok := it.(item.Storage); ok { info := x.StorageInfo() if info.NumViewableSlots != 0 { - builder.AddComponent("minecraft:bundle_interaction", map[string]any{ + if info.NumViewableSlots < 1 || info.NumViewableSlots > 64 { + return nil, fmt.Errorf("NumViewableSlots %d out of range 1-64.", info.NumViewableSlots) + } + builder.AddComponent("bundle_interaction", map[string]any{ "num_viewable_slots": int32(info.NumViewableSlots), }) } if info.MaxSlots != 0 || len(info.AllowedItems) != 0 || len(info.BannedItems) != 0 { - builder.AddComponent("minecraft:storage_item", map[string]any{ + builder.AddComponent("storage_item", map[string]any{ "allow_nested_storage_items": info.AllowNestedStorageItems, "allowed_items": stringSlice(info.AllowedItems), "banned_items": bannedItems(info.BannedItems), @@ -188,12 +198,12 @@ func Components(it world.CustomItem) map[string]any { }) } if info.MaxWeightLimit != 0 { - builder.AddComponent("minecraft:storage_weight_limit", map[string]any{ + builder.AddComponent("storage_weight_limit", map[string]any{ "max_weight_limit": int32(info.MaxWeightLimit), }) } if info.WeightInStorageItem != 0 { - builder.AddComponent("minecraft:storage_weight_modifier", map[string]any{ + builder.AddComponent("storage_weight_modifier", map[string]any{ "weight_in_storage_item": int32(info.WeightInStorageItem), }) } @@ -211,28 +221,28 @@ func Components(it world.CustomItem) map[string]any { if info.StartUsing != "" { modifiers["start_using"] = info.StartUsing } - builder.AddComponent("minecraft:use_modifiers", modifiers) + builder.AddComponent("use_modifiers", modifiers) } if x, ok := it.(item.SwingDuration); ok { - builder.AddComponent("minecraft:swing_duration", map[string]any{ + builder.AddComponent("swing_duration", map[string]any{ "value": float32(x.SwingDuration()), }) } if x, ok := it.(item.SwingSounds); ok { info := x.SwingSounds() - builder.AddComponent("minecraft:swing_sounds", map[string]any{ + builder.AddComponent("swing_sounds", map[string]any{ "attack_hit": info.AttackHit, "attack_miss": info.AttackMiss, }) } if x, ok := it.(item.KineticWeapon); ok { - builder.AddComponent("minecraft:kinetic_weapon", map[string]any{ - "minecraft:kinetic_weapon": kineticWeaponData(x.KineticWeaponInfo()), + builder.AddComponent("kinetic_weapon", map[string]any{ + "kinetic_weapon": kineticWeaponData(x.KineticWeaponInfo()), }) } if x, ok := it.(item.PiercingWeapon); ok { info := x.PiercingWeaponInfo() - builder.AddComponent("minecraft:piercing_weapon", map[string]any{ + builder.AddComponent("piercing_weapon", map[string]any{ "creative_reach": rangeData(info.CreativeReach), "hitbox_margin": float32(info.HitboxMargin), "reach": rangeData(info.Reach), @@ -240,7 +250,7 @@ func Components(it world.CustomItem) map[string]any { } if x, ok := it.(item.Camera); ok { info := x.CameraInfo() - builder.AddComponent("minecraft:camera", map[string]any{ + builder.AddComponent("camera", map[string]any{ "black_bars_duration": float32(info.BlackBarsDuration), "black_bars_screen_ratio": float32(info.BlackBarsScreenRatio), "picture_duration": float32(info.PictureDuration), @@ -248,15 +258,109 @@ func Components(it world.CustomItem) map[string]any { "shutter_screen_ratio": float32(info.ShutterScreenRatio), "slide_away_duration": float32(info.SlideAwayDuration), }) - builder.AddComponent("minecraft:block", "minecraft:camera") + builder.AddComponent("block", "camera") if info.UseDuration != 0 { builder.AddProperty("use_duration", int32(info.UseDuration)) } } - return builder.Construct() + if x, ok := it.(item.BlockPlacer); ok { + info := x.BlockPlacerInfo() + blockPlacer := map[string]any{ + "block": info.Block, + "replace_block_item": info.ReplaceBlockItem, + "aligned_placement": info.AlignedPlacement, + } + if len(info.UseOn) != 0 { + blockPlacer["use_on"] = stringSlice(info.UseOn) + } + builder.AddComponent("block_placer", blockPlacer) + } + if x, ok := it.(item.Compostable); ok { + builder.AddComponent("compostable", map[string]any{ + "composting_chance": int32(x.CompostChance() * 100), + }) + } + if x, ok := it.(item.DamageAbsorption); ok { + builder.AddComponent("damage_absorption", map[string]any{ + "absorbable_causes": stringSlice(x.AbsorbableCauses()), + }) + } + if x, ok := it.(item.Digger); ok { + info := x.DiggerInfo() + speeds := make([]any, 0, len(info.DestroySpeeds)) + for _, ds := range info.DestroySpeeds { + speeds = append(speeds, map[string]any{ + "block": ds.Block, + "speed": float32(ds.Speed), + }) + } + builder.AddComponent("digger", map[string]any{ + "destroy_speeds": speeds, + "use_efficiency": info.UseEfficiency, + }) + } + if x, ok := it.(item.DurabilitySensor); ok { + info := x.DurabilitySensorInfo() + sensor := map[string]any{} + if info.SoundEvent != "" { + sensor["sound_event"] = info.SoundEvent + } + if len(info.DurabilityThresholds) != 0 { + sensor["durability_thresholds"] = durabilityThresholds(info.DurabilityThresholds) + } + builder.AddComponent("durability_sensor", sensor) + } + if x, ok := it.(item.Dyeable); ok { + c := x.DefaultColor() + builder.AddComponent("dyeable", map[string]any{ + "default_color": []any{int32(c[0]), int32(c[1]), int32(c[2])}, + }) + } + if x, ok := it.(item.EntityPlacer); ok { + info := x.EntityPlacerInfo() + entityPlacer := map[string]any{ + "entity": info.Entity, + } + if len(info.UseOn) != 0 { + entityPlacer["use_on"] = stringSlice(info.UseOn) + } + if len(info.DispenseOn) != 0 { + entityPlacer["dispense_on"] = stringSlice(info.DispenseOn) + } + builder.AddComponent("entity_placer", entityPlacer) + } + if x, ok := it.(item.HoverTextColor); ok { + builder.AddComponent("hover_text_color", map[string]any{ + "value": x.HoverTextColor(), + }) + } + if x, ok := it.(item.InteractButton); ok { + builder.AddComponent("interact_button", map[string]any{ + "value": x.InteractButton(), + }) + } + if x, ok := it.(item.LiquidClipped); ok { + builder.AddComponent("liquid_clipped", map[string]any{ + "value": x.LiquidClipped(), + }) + } + if x, ok := it.(item.Rarity); ok { + builder.AddComponent("rarity", map[string]any{ + "value": x.Rarity(), + }) + } + if x, ok := it.(item.Record); ok { + info := x.RecordInfo() + builder.AddComponent("record", map[string]any{ + "comparator_signal": int32(info.ComparatorSignal), + "duration": float32(info.Duration), + "sound_event": info.SoundEvent, + }) + } + return builder.Construct(), nil } -// repairItems converts the repair materials of an item to the data required for the minecraft:repairable +// repairItems converts the repair materials of an item to the data required for the repairable // component. func repairItems(items []item.RepairItem) []any { materials := make([]any, 0, len(items)) @@ -276,7 +380,7 @@ func repairItems(items []item.RepairItem) []any { } // bannedItems converts the banned item identifiers of a storage item to the data required for the -// minecraft:storage_item component. +// storage_item component. func bannedItems(items []string) []any { banned := make([]any, 0, len(items)) for _, b := range items { @@ -294,7 +398,7 @@ func rangeData(r [2]float64) map[string]any { } // kineticWeaponData converts the kinetic weapon information of an item to the data required for the -// minecraft:kinetic_weapon component. +// kinetic_weapon component. func kineticWeaponData(info item.KineticWeaponInfo) map[string]any { conditions := func(c item.WeaponConditions) map[string]any { return map[string]any{ @@ -324,3 +428,22 @@ func stringSlice(x []string) []any { } return s } + +// durabilityThresholds converts a slice of durability thresholds to the data required for the +// durability_sensor component. +func durabilityThresholds(thresholds []item.DurabilityThreshold) []any { + t := make([]any, 0, len(thresholds)) + for _, th := range thresholds { + m := map[string]any{ + "durability": int32(th.Durability), + } + if th.ParticleType != "" { + m["particle_type"] = th.ParticleType + } + if th.SoundEvent != "" { + m["sound_event"] = th.SoundEvent + } + t = append(t, m) + } + return t +} diff --git a/server/item/armour.go b/server/item/armour.go index 17f44a5392..afbf67272f 100644 --- a/server/item/armour.go +++ b/server/item/armour.go @@ -16,6 +16,10 @@ type ( // resisted upon being attacked. 1 knock back resistance point client-side translates to 10% knock back // reduction. KnockBackResistance() float64 + // HidesPlayerLocation returns a boolean that determines whether the Player's location is hidden on Locator Maps + // and the Locator Bar when the wearable item is worn. Default is false. + HidesPlayerLocation() bool + // Dispensable() bool } // ArmourTier represents the tier, or material, that a piece of armour is made of. ArmourTier interface { diff --git a/server/item/block_placer.go b/server/item/block_placer.go new file mode 100644 index 0000000000..ff89d4008c --- /dev/null +++ b/server/item/block_placer.go @@ -0,0 +1,22 @@ +package item + +// BlockPlacer represents an item that can place blocks when used. +type BlockPlacer interface { + // BlockPlacerInfo returns the block placer information of the item. + BlockPlacerInfo() BlockPlacerInfo +} + +// BlockPlacerInfo is a struct returned by items that implement BlockPlacer. It contains the block +// placement configuration of the item. +type BlockPlacerInfo struct { + // Block is the identifier of the block that will be placed. + Block string + // UseOn is a list of block identifiers that this item can be used on. If empty, all blocks + // are allowed. + UseOn []string + // ReplaceBlockItem specifies if the item will be registered as the item for this block. + ReplaceBlockItem bool + // AlignedPlacement specifies if block placement through this item is aligned while the + // interaction button is held down. + AlignedPlacement bool +} diff --git a/server/item/damage_absorption.go b/server/item/damage_absorption.go new file mode 100644 index 0000000000..5ca4c5ab3d --- /dev/null +++ b/server/item/damage_absorption.go @@ -0,0 +1,8 @@ +package item + +// DamageAbsorption represents an item that can absorb damage that would otherwise be dealt to its +// wearer. The item needs a minecraft:durability component for this to function. +type DamageAbsorption interface { + // AbsorbableCauses returns the list of damage causes that can be absorbed by the item. + AbsorbableCauses() []string +} diff --git a/server/item/digger.go b/server/item/digger.go new file mode 100644 index 0000000000..f643cf1c41 --- /dev/null +++ b/server/item/digger.go @@ -0,0 +1,26 @@ +package item + +// Digger represents an item configured as a digging tool, allowing it to break specific blocks +// faster than normal. +type Digger interface { + // DiggerInfo returns the digger information of the item. + DiggerInfo() DiggerInfo +} + +// DiggerInfo is a struct returned by items that implement Digger. It contains the block-specific +// mining speed configuration of the item. +type DiggerInfo struct { + // DestroySpeeds is a list of block-specific mining speed multipliers. + DestroySpeeds []DestroySpeed + // UseEfficiency specifies if the Efficiency enchantment will increase the dig speed of this + // item. + UseEfficiency bool +} + +// DestroySpeed associates a block with a custom digging speed multiplier. +type DestroySpeed struct { + // Block is the identifier of the block that can be dug. + Block string + // Speed is the digging speed multiplier for the correlating block. + Speed float64 +} diff --git a/server/item/durability_sensor.go b/server/item/durability_sensor.go new file mode 100644 index 0000000000..f551c20369 --- /dev/null +++ b/server/item/durability_sensor.go @@ -0,0 +1,29 @@ +package item + +// DurabilitySensor represents an item that emits effects when it receives damage. The item also +// needs a minecraft:durability component. +type DurabilitySensor interface { + // DurabilitySensorInfo returns the durability sensor information of the item. + DurabilitySensorInfo() DurabilitySensorInfo +} + +// DurabilitySensorInfo is a struct returned by items that implement DurabilitySensor. It contains +// the thresholds and effects emitted when durability is reduced. +type DurabilitySensorInfo struct { + // SoundEvent is the sound effect emitted when any threshold is met. + SoundEvent string + // DurabilityThresholds is a list of thresholds at which effects are emitted. + DurabilityThresholds []DurabilityThreshold +} + +// DurabilityThreshold defines the durability threshold and effects emitted when that threshold +// is met. +type DurabilityThreshold struct { + // Durability is the durability value at which effects are emitted. Effects are emitted when + // the item durability value is less than or equal to this value. + Durability int + // ParticleType is the particle effect to emit when the threshold is met. + ParticleType string + // SoundEvent is the sound effect to emit when the threshold is met. + SoundEvent string +} diff --git a/server/item/dyeable.go b/server/item/dyeable.go new file mode 100644 index 0000000000..f776fae7a9 --- /dev/null +++ b/server/item/dyeable.go @@ -0,0 +1,7 @@ +package item + +// Dyeable represents an item that can be dyed using dyes in a crafting grid, like leather armor. +type Dyeable interface { + // DefaultColor returns the default RGB color of the item when undyed. + DefaultColor() [3]uint8 +} diff --git a/server/item/enchantable_data.go b/server/item/enchantable_data.go index 336b18605d..942a5a40b2 100644 --- a/server/item/enchantable_data.go +++ b/server/item/enchantable_data.go @@ -13,5 +13,5 @@ type EnchantableInfo struct { // Slot is the enchantment slot of the item, such as "melee_spear" or "sword". Slot string // Value is the enchantment value of the item. - Value int + Value uint } diff --git a/server/item/entity_placer.go b/server/item/entity_placer.go new file mode 100644 index 0000000000..82fbcd647a --- /dev/null +++ b/server/item/entity_placer.go @@ -0,0 +1,20 @@ +package item + +// EntityPlacer represents an item that can place entities into the world, such as spawn eggs. +type EntityPlacer interface { + // EntityPlacerInfo returns the entity placer information of the item. + EntityPlacerInfo() EntityPlacerInfo +} + +// EntityPlacerInfo is a struct returned by items that implement EntityPlacer. It contains the +// entity placement configuration of the item. +type EntityPlacerInfo struct { + // Entity is the identifier of the entity that will be placed. + Entity string + // UseOn is a list of block identifiers that this item can be used on. If empty, all blocks + // are allowed. + UseOn []string + // DispenseOn is a list of block identifiers that this item can be dispensed on. If empty, + // all blocks are allowed. + DispenseOn []string +} diff --git a/server/item/hover_text_color.go b/server/item/hover_text_color.go new file mode 100644 index 0000000000..a0d2e1a555 --- /dev/null +++ b/server/item/hover_text_color.go @@ -0,0 +1,7 @@ +package item + +// HoverTextColor represents an item with a custom hover text color. +type HoverTextColor interface { + // HoverTextColor returns the color of the item name when hovering over it. + HoverTextColor() string +} diff --git a/server/item/interact_button.go b/server/item/interact_button.go new file mode 100644 index 0000000000..4eebcfb726 --- /dev/null +++ b/server/item/interact_button.go @@ -0,0 +1,8 @@ +package item + +// InteractButton represents an item that shows an interact button in touch controls. +type InteractButton interface { + // InteractButton returns the text displayed on the interact button. If true, the default + // "Use Item" text will be used. + InteractButton() string +} diff --git a/server/item/liquid_clipped.go b/server/item/liquid_clipped.go new file mode 100644 index 0000000000..41bf96afbb --- /dev/null +++ b/server/item/liquid_clipped.go @@ -0,0 +1,7 @@ +package item + +// LiquidClipped represents an item that interacts with liquid blocks on use. +type LiquidClipped interface { + // LiquidClipped returns whether the item interacts with liquid blocks on use. + LiquidClipped() bool +} diff --git a/server/item/rarity.go b/server/item/rarity.go new file mode 100644 index 0000000000..57b061f0d0 --- /dev/null +++ b/server/item/rarity.go @@ -0,0 +1,9 @@ +package item + +// Rarity represents an item with a specific base rarity that determines the color of the item name +// when hovering over it. +type Rarity interface { + // Rarity returns the base rarity of the item. Valid values are "common", "uncommon", "rare", + // and "epic". + Rarity() string +} diff --git a/server/item/record.go b/server/item/record.go new file mode 100644 index 0000000000..56ba531afc --- /dev/null +++ b/server/item/record.go @@ -0,0 +1,18 @@ +package item + +// Record represents an item that can play music when placed in a jukebox. +type Record interface { + // RecordInfo returns the record information of the item. + RecordInfo() RecordInfo +} + +// RecordInfo is a struct returned by items that implement Record. It contains the music playback +// configuration of the item. +type RecordInfo struct { + // ComparatorSignal is the signal strength for comparator blocks, from 1 to 13. + ComparatorSignal int + // Duration is the duration of the sound event in seconds. + Duration float64 + // SoundEvent is the sound event played by the record. + SoundEvent string +} diff --git a/server/item/storage.go b/server/item/storage.go index b543598ec8..7f57ac720d 100644 --- a/server/item/storage.go +++ b/server/item/storage.go @@ -16,8 +16,8 @@ type StorageInfo struct { // WeightInStorageItem is the weight of the item itself when stored inside another storage item. WeightInStorageItem int // NumViewableSlots is the number of slots that may be viewed when interacting with the item. If set to - // zero, no bundle interaction component is sent. - NumViewableSlots int + // zero, no bundle interaction component is sent. Default is 12. Value must be >= 1. Value must be <= 64. + NumViewableSlots uint8 // AllowNestedStorageItems is true if other storage items may be stored inside the item. AllowNestedStorageItems bool // AllowedItems is a list of item identifiers that may be stored inside the item. diff --git a/server/server.go b/server/server.go index 574515bc58..819718bc9c 100644 --- a/server/server.go +++ b/server/server.go @@ -92,7 +92,7 @@ func New() *Server { // Listen starts running the server's listeners. Connections will be accepted // until the listeners are closed using a call to Close. Once Listen is called, // players may be accepted using Server.Accept(). -func (srv *Server) Listen() { +func (srv *Server) Listen() error { t := time.Now() if !srv.started.CompareAndSwap(nil, &t) { panic("start server: already started") @@ -110,8 +110,11 @@ func (srv *Server) Listen() { } srv.conf.Log.Info("Dragonfly server started.", "mc-version", protocol.CurrentVersion, "go-version", info.GoVersion, "commit", revision) - srv.startListening() + if err := srv.startListening(); err != nil { + return err + } go srv.wait() + return nil } // Accept accepts incoming players into the server, returning an iterator that @@ -381,15 +384,18 @@ func (srv *Server) listen(l Listener) { // startListening starts making the EncodeBlock listener listen, accepting new // connections from players. -func (srv *Server) startListening() { +func (srv *Server) startListening() error { srv.makeBlockEntries() - srv.makeItemComponents() + if err := srv.makeItemComponents(); err != nil { + return err + } srv.makeDimensionData() srv.wg.Add(len(srv.listeners)) for _, l := range srv.listeners { go srv.listen(l) } + return nil } // makeBlockEntries initialises the server's block components map using the @@ -411,7 +417,7 @@ func (srv *Server) makeBlockEntries() { // makeItemComponents initialises the server's item components map using the // registered custom items. It allows item components to be created only once // at startup -func (srv *Server) makeItemComponents() { +func (srv *Server) makeItemComponents() error { custom := world.CustomItems() srv.customItems = make([]protocol.ItemEntry, len(custom)) @@ -423,14 +429,21 @@ func (srv *Server) makeItemComponents() { if isCustomBlock { entryVersion = protocol.ItemEntryVersionNone } + + components, err := iteminternal.Components(it) + if err != nil { + return fmt.Errorf("failed to register components for %s: %w", name, err) + } + srv.customItems[i] = protocol.ItemEntry{ Name: name, ComponentBased: !isCustomBlock, RuntimeID: int16(rid), Version: entryVersion, - Data: iteminternal.Components(it), + Data: components, } } + return nil } // makeDimensionData initialises the server's custom dimensions list. From a146371e4d8e699b0c262b82aef115ec51911193 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:02:49 -0600 Subject: [PATCH 04/13] fix typo --- server/internal/iteminternal/components.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index b2a0bfb2c6..206122cc37 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -16,7 +16,7 @@ func Components(it world.CustomItem) (map[string]any, error) { parts := strings.SplitN(identifier, ":", 1) if len(parts) < 2 { - return nil, fmt.Errorf("indetifier %s must contain namespace.", identifier) + return nil, fmt.Errorf("identifier %s must contain namespace.", identifier) } name := parts[1] From f598b8a8abf14db5b9f9b13c80bf6e0e270fbd93 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:59:44 -0600 Subject: [PATCH 05/13] builder.go: Revert unintended change --- server/internal/iteminternal/builder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/internal/iteminternal/builder.go b/server/internal/iteminternal/builder.go index 5690d3b648..cc74467294 100644 --- a/server/internal/iteminternal/builder.go +++ b/server/internal/iteminternal/builder.go @@ -66,7 +66,7 @@ func (builder *ComponentBuilder) applyDefaultProperties(x map[string]any) { // applyDefaultComponents applies the default components to the provided map. It is important that this method does not // modify the builder's components map directly otherwise Empty() will return false in future use of the builder. func (builder *ComponentBuilder) applyDefaultComponents(x, properties map[string]any) { - x["components"] = properties + x["item_properties"] = properties x["minecraft:display_name"] = map[string]any{ "value": builder.name, } From a24c2885751e4401fed839b05caca782f602c394 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:46:01 -0600 Subject: [PATCH 06/13] fix error messages --- server/internal/iteminternal/components.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index 206122cc37..035645c0c5 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -16,7 +16,7 @@ func Components(it world.CustomItem) (map[string]any, error) { parts := strings.SplitN(identifier, ":", 1) if len(parts) < 2 { - return nil, fmt.Errorf("identifier %s must contain namespace.", identifier) + return nil, fmt.Errorf("identifier %s must contain namespace", identifier) } name := parts[1] @@ -183,7 +183,7 @@ func Components(it world.CustomItem) (map[string]any, error) { info := x.StorageInfo() if info.NumViewableSlots != 0 { if info.NumViewableSlots < 1 || info.NumViewableSlots > 64 { - return nil, fmt.Errorf("NumViewableSlots %d out of range 1-64.", info.NumViewableSlots) + return nil, fmt.Errorf("NumViewableSlots %d out of range 1-64", info.NumViewableSlots) } builder.AddComponent("bundle_interaction", map[string]any{ "num_viewable_slots": int32(info.NumViewableSlots), From 602f68ea32b1012f61698eaba3308780479617c1 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:41:41 -0600 Subject: [PATCH 07/13] server: expanded component integration per docs. --- server/internal/iteminternal/builder.go | 12 +-- server/internal/iteminternal/components.go | 87 ++++++++++++++++++++-- server/item/armour.go | 4 +- server/item/durability.go | 3 + server/item/icon.go | 9 +++ server/item/item.go | 21 +++++- server/item/projectile.go | 18 +++++ server/item/shooter.go | 37 +++++++++ server/item/should_despawn.go | 7 ++ server/item/swing.go | 2 + 10 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 server/item/icon.go create mode 100644 server/item/projectile.go create mode 100644 server/item/shooter.go create mode 100644 server/item/should_despawn.go diff --git a/server/internal/iteminternal/builder.go b/server/internal/iteminternal/builder.go index cc74467294..fe4a3da0e8 100644 --- a/server/internal/iteminternal/builder.go +++ b/server/internal/iteminternal/builder.go @@ -51,13 +51,15 @@ func (builder *ComponentBuilder) Construct() map[string]any { // applyDefaultProperties applies the default properties to the provided map. It is important that this method does // not modify the builder's properties map directly otherwise Empty() will return false in future use of the builder. func (builder *ComponentBuilder) applyDefaultProperties(x map[string]any) { - x["minecraft:icon"] = map[string]any{ - "textures": map[string]any{ - "default": builder.identifier, - }, - } x["creative_group"] = builder.category.Group() x["creative_category"] = int32(builder.category.Uint8()) + if _, ok := x["minecraft:icon"]; !ok { + x["minecraft:icon"] = map[string]any{ + "textures": map[string]any{ + "default": builder.identifier, + }, + } + } if _, ok := x["max_stack_size"]; !ok { x["max_stack_size"] = int32(64) } diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index 035645c0c5..86d1b17479 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -38,6 +38,7 @@ func Components(it world.CustomItem) (map[string]any, error) { "slot": slot, "protection": int32(x.DefencePoints()), "hides_player_location": x.HidesPlayerLocation(), + "dispensable": x.Dispensable(), }) } if x, ok := it.(item.Consumable); ok { @@ -102,13 +103,31 @@ func Components(it world.CustomItem) (map[string]any, error) { builder.AddComponent("cooldown", cooldown) } if x, ok := it.(item.Durable); ok { + info := x.DurabilityInfo() + damageChance := map[string]any{ + "min": int32(100), + "max": int32(100), + } + if info.DamageChance != [2]int{} { + damageChance["min"] = int32(info.DamageChance[0]) + damageChance["max"] = int32(info.DamageChance[1]) + } builder.AddComponent("durability", map[string]any{ - "max_durability": int32(x.DurabilityInfo().MaxDurability), + "max_durability": int32(info.MaxDurability), + "damage_chance": damageChance, }) } if x, ok := it.(item.MaxCounter); ok { builder.AddProperty("max_stack_size", int32(x.MaxCount())) } + if x, ok := it.(item.Icon); ok { + textures := x.IconTextures() + m := make(map[string]any, len(textures)) + for k, v := range textures { + m[k] = v + } + builder.AddProperty("minecraft:icon", map[string]any{"textures": m}) + } if x, ok := it.(item.OffHand); ok { builder.AddProperty("allow_off_hand", x.OffHand()) } @@ -124,11 +143,36 @@ func Components(it world.CustomItem) (map[string]any, error) { if x, ok := it.(item.CanDestroyInCreative); ok { builder.AddProperty("can_destroy_in_creative", x.CanDestroyInCreative()) } - if x, ok := it.(item.Throwable); ok { - builder.AddComponent("projectile", map[string]any{}) - builder.AddComponent("throwable", map[string]any{ - "do_swing_animation": x.SwingAnimation(), + if x, ok := it.(item.Projectile); ok { + info := x.ProjectileInfo() + builder.AddComponent("projectile", map[string]any{ + "minimum_critical_power": float32(info.MinimumCriticalPower), + "projectile_entity": info.ProjectileEntity, }) + } else if _, ok := it.(item.Throwable); ok { + builder.AddComponent("projectile", map[string]any{}) + } + if x, ok := it.(item.Throwable); ok { + info := x.ThrowableInfo() + throwable := map[string]any{ + "do_swing_animation": info.SwingAnimation, + } + if info.LaunchPowerScale != 0 { + throwable["launch_power_scale"] = float32(info.LaunchPowerScale) + } + if info.MaxDrawDuration != 0 { + throwable["max_draw_duration"] = float32(info.MaxDrawDuration) + } + if info.MaxLaunchPower != 0 { + throwable["max_launch_power"] = float32(info.MaxLaunchPower) + } + if info.MinDrawDuration != 0 { + throwable["min_draw_duration"] = float32(info.MinDrawDuration) + } + if info.ScalePowerByDrawDuration { + throwable["scale_power_by_draw_duration"] = true + } + builder.AddComponent("throwable", throwable) } if x, ok := it.(item.Glinted); ok { builder.AddComponent("glint", map[string]any{ @@ -231,8 +275,9 @@ func Components(it world.CustomItem) (map[string]any, error) { if x, ok := it.(item.SwingSounds); ok { info := x.SwingSounds() builder.AddComponent("swing_sounds", map[string]any{ - "attack_hit": info.AttackHit, - "attack_miss": info.AttackMiss, + "attack_critical_hit": info.AttackCriticalHit, + "attack_hit": info.AttackHit, + "attack_miss": info.AttackMiss, }) } if x, ok := it.(item.KineticWeapon); ok { @@ -357,9 +402,37 @@ func Components(it world.CustomItem) (map[string]any, error) { "sound_event": info.SoundEvent, }) } + if x, ok := it.(item.Shooter); ok { + info := x.ShooterInfo() + builder.AddComponent("shooter", map[string]any{ + "ammunition": ammunition(info.Ammunition), + "charge_on_draw": info.ChargeOnDraw, + "max_draw_duration": float32(info.MaxDrawDuration), + "scale_power_by_draw_duration": info.ScalePowerByDrawDuration, + }) + } + if x, ok := it.(item.ShouldDespawn); ok { + builder.AddComponent("should_despawn", map[string]any{ + "value": x.ShouldDespawn(), + }) + } return builder.Construct(), nil } +// ammunition converts a slice of ammunition to the data required for the shooter component. +func ammunition(items []item.Ammunition) []any { + ammo := make([]any, 0, len(items)) + for _, a := range items { + ammo = append(ammo, map[string]any{ + "item": a.Item, + "search_inventory": a.SearchInventory, + "use_in_creative": a.UseInCreative, + "use_offhand": a.UseOffHand, + }) + } + return ammo +} + // repairItems converts the repair materials of an item to the data required for the repairable // component. func repairItems(items []item.RepairItem) []any { diff --git a/server/item/armour.go b/server/item/armour.go index afbf67272f..148b2f8146 100644 --- a/server/item/armour.go +++ b/server/item/armour.go @@ -19,7 +19,9 @@ type ( // HidesPlayerLocation returns a boolean that determines whether the Player's location is hidden on Locator Maps // and the Locator Bar when the wearable item is worn. Default is false. HidesPlayerLocation() bool - // Dispensable() bool + // Dispensable returns a boolean that determines whether the wearable item can be dispensed by dispensers, + // equipping it on the target. Default is false. + Dispensable() bool } // ArmourTier represents the tier, or material, that a piece of armour is made of. ArmourTier interface { diff --git a/server/item/durability.go b/server/item/durability.go index eff07c0f60..213328de5c 100644 --- a/server/item/durability.go +++ b/server/item/durability.go @@ -19,6 +19,9 @@ type DurabilityInfo struct { // AttackDurability and BreakDurability are the losses in durability that the item sustains when they are // used to do the respective actions. AttackDurability, BreakDurability int + // DamageChance is the percentage chance of the item losing durability when damaged, as a min and max value. + // Both values default to 100 if unset. + DamageChance [2]int // Persistent is true if the item is persistent, i.e. it will not be destroyed when at its last durability stage. Persistent bool } diff --git a/server/item/icon.go b/server/item/icon.go new file mode 100644 index 0000000000..93a66b0bd1 --- /dev/null +++ b/server/item/icon.go @@ -0,0 +1,9 @@ +package item + +// Icon represents an item with a custom icon texture. The icon textures are the keys from the +// resource_pack/textures/item_texture.json 'texture_data' object associated with the texture file. +type Icon interface { + // IconTextures returns the textures used for the item's icon. The "default" key contains the actual icon + // texture of the item. Additional keys may be used to specify armour trim textures and palettes. + IconTextures() map[string]string +} diff --git a/server/item/item.go b/server/item/item.go index 8736cc78c5..205021fa83 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -51,8 +51,25 @@ type Usable interface { // Throwable represents a custom item that can be thrown such as a projectile. This will only have an effect on // non-vanilla items. type Throwable interface { - // SwingAnimation returns true if the client should cause the player's arm to swing when the item is thrown. - SwingAnimation() bool + // ThrowableInfo returns the throwable information of the item. + ThrowableInfo() ThrowableInfo +} + +// ThrowableInfo is a struct returned by items that implement Throwable. It contains the information required for +// the client to throw the item. +type ThrowableInfo struct { + // SwingAnimation is true if the client should cause the player's arm to swing when the item is thrown. + SwingAnimation bool + // LaunchPowerScale is the scale at which the power of the throw increases. + LaunchPowerScale float64 + // MaxDrawDuration is the maximum duration to draw a throwable item, in seconds. + MaxDrawDuration float64 + // MaxLaunchPower is the maximum power to launch the throwable item. + MaxLaunchPower float64 + // MinDrawDuration is the minimum duration to draw a throwable item, in seconds. + MinDrawDuration float64 + // ScalePowerByDrawDuration is true if the power of the throw increases with the duration charged. + ScalePowerByDrawDuration bool } // OffHand represents an item that can be held in the off hand. diff --git a/server/item/projectile.go b/server/item/projectile.go new file mode 100644 index 0000000000..dfb1cd8e6f --- /dev/null +++ b/server/item/projectile.go @@ -0,0 +1,18 @@ +package item + +// Projectile represents an item that is a projectile, which may be shot from dispensers or used as ammunition by +// items implementing Shooter. When combined with Throwable, this specifies the entity spawned when the item is +// thrown. +type Projectile interface { + // ProjectileInfo returns the projectile information of the item. + ProjectileInfo() ProjectileInfo +} + +// ProjectileInfo is a struct returned by items that implement Projectile. It contains the information required for +// the client to use the item as a projectile. +type ProjectileInfo struct { + // ProjectileEntity is the identifier of the entity fired as a projectile. + ProjectileEntity string + // MinimumCriticalPower is how long a player must charge a projectile for it to critically hit, in seconds. + MinimumCriticalPower float64 +} diff --git a/server/item/shooter.go b/server/item/shooter.go new file mode 100644 index 0000000000..181b5ae46d --- /dev/null +++ b/server/item/shooter.go @@ -0,0 +1,37 @@ +package item + +// Shooter represents an item that is able to shoot projectiles, similarly to a bow or crossbow. Ammunition used by +// the item must implement Projectile in order to function properly. +type Shooter interface { + // ShooterInfo returns the shooter information of the item. + ShooterInfo() ShooterInfo +} + +// ShooterInfo is a struct returned by items that implement Shooter. It contains the information required for the +// client to shoot projectiles using the item. +type ShooterInfo struct { + // Ammunition is a list of ammunition entries that define which items can be used as projectiles for this + // shooter. + Ammunition []Ammunition + // MaxDrawDuration is the maximum time in seconds that a player can draw the shooter before it automatically + // fires or reaches maximum power. + MaxDrawDuration float64 + // ChargeOnDraw is true if the shooter begins charging when the player starts drawing, similar to a crossbow. + ChargeOnDraw bool + // ScalePowerByDrawDuration is true if the projectile's launch power increases based on how long the player + // holds the use button before releasing. + ScalePowerByDrawDuration bool +} + +// Ammunition represents an entry of ammunition that can be used by a Shooter. It specifies the item to be used as +// a projectile and where it may be taken from. +type Ammunition struct { + // Item is the identifier of the item used as ammunition. + Item string + // SearchInventory is true if the inventory of the user may be searched for the ammunition. + SearchInventory bool + // UseInCreative is true if the ammunition may be used in creative mode. + UseInCreative bool + // UseOffHand is true if the off-hand of the user may be used for the ammunition. + UseOffHand bool +} diff --git a/server/item/should_despawn.go b/server/item/should_despawn.go new file mode 100644 index 0000000000..09fcdb6f8f --- /dev/null +++ b/server/item/should_despawn.go @@ -0,0 +1,7 @@ +package item + +// ShouldDespawn represents an item that has a configurable despawn behaviour while floating in the world. +type ShouldDespawn interface { + // ShouldDespawn returns whether the item should eventually despawn while floating in the world. + ShouldDespawn() bool +} diff --git a/server/item/swing.go b/server/item/swing.go index 2a6250a518..01ae5eb9ae 100644 --- a/server/item/swing.go +++ b/server/item/swing.go @@ -16,6 +16,8 @@ type SwingSounds interface { // SwingSoundsInfo is a struct returned by items that implement SwingSounds. It contains the sounds played // when the item is swung. type SwingSoundsInfo struct { + // AttackCriticalHit is the sound played when an attack made with the item hits and deals critical damage. + AttackCriticalHit string // AttackHit is the sound played when an attack made with the item hits. AttackHit string // AttackMiss is the sound played when an attack made with the item misses. From 7eac2e437a19371775035216925d1b6ebe88497d Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:13:05 -0600 Subject: [PATCH 08/13] component: generate item components from a manifest --- .github/workflows/pr.yml | 2 +- .github/workflows/push.yml | 2 +- .../componentgen/generate/components.go | 311 +++++++ .../componentgen/generate/generator.go | 191 ++++ .../componentgen/generate/manifest.go | 501 ++++++++++ cmd/generate/componentgen/main.go | 58 ++ cmd/generate/componentgen/parse/schemas.go | 122 +++ cmd/generate/componentgen/parse/vanilla.go | 29 + cmd/generate/generator/runner.go | 89 ++ cmd/generate/main.go | 10 + go.mod | 4 + go.sum | 6 + server/internal/iteminternal/components.go | 133 ++- server/internal/iteminternal/golden_test.go | 82 ++ .../internal/iteminternal/validation_test.go | 164 ++++ server/internal/iteminternal/validator.go | 80 ++ server/internal/nbtconv/read.go | 2 +- server/item/armour.go | 6 - server/item/component/component.go | 26 + server/item/component/component_test.go | 308 +++++++ server/item/component/components_gen.go | 852 ++++++++++++++++++ server/item/component/constants.go | 46 + server/item/component/example_test.go | 65 ++ server/item/component/validation.go | 41 + server/item/tags.go | 101 +++ 25 files changed, 3173 insertions(+), 58 deletions(-) create mode 100644 cmd/generate/componentgen/generate/components.go create mode 100644 cmd/generate/componentgen/generate/generator.go create mode 100644 cmd/generate/componentgen/generate/manifest.go create mode 100644 cmd/generate/componentgen/main.go create mode 100644 cmd/generate/componentgen/parse/schemas.go create mode 100644 cmd/generate/componentgen/parse/vanilla.go create mode 100644 cmd/generate/generator/runner.go create mode 100644 cmd/generate/main.go create mode 100644 server/internal/iteminternal/golden_test.go create mode 100644 server/internal/iteminternal/validation_test.go create mode 100644 server/internal/iteminternal/validator.go create mode 100644 server/item/component/component.go create mode 100644 server/item/component/component_test.go create mode 100644 server/item/component/components_gen.go create mode 100644 server/item/component/constants.go create mode 100644 server/item/component/example_test.go create mode 100644 server/item/component/validation.go diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d41b822aee..592cc5a0df 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,7 +23,7 @@ jobs: - name: Verify generated code run: | - go generate ./server/block/ + go generate ./server/block/ ./server/item/component/ git diff --exit-code - name: Build deployment artifact diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b4c2019275..ace04b35e6 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -23,7 +23,7 @@ jobs: - name: Verify generated code run: | - go generate ./server/block/ + go generate ./server/block/ ./server/item/component/ git diff --exit-code deploy: diff --git a/cmd/generate/componentgen/generate/components.go b/cmd/generate/componentgen/generate/components.go new file mode 100644 index 0000000000..73223d0b80 --- /dev/null +++ b/cmd/generate/componentgen/generate/components.go @@ -0,0 +1,311 @@ +package generate + +import ( + "bytes" + "fmt" + "go/format" + "os" + "path/filepath" + "strings" +) + +// generateComponentImplementations writes a single file containing every component +// struct definition plus its ComponentName() and Encode() methods, sourced from +// the manifest. Helper transform functions used by the encoders are emitted once. +func (g *Generator) generateComponentImplementations() error { + var buf bytes.Buffer + buf.WriteString("// Code generated by componentgen. DO NOT EDIT.\n") + buf.WriteString("package component\n\n") + + // Collect nested types (name -> fields) referenced by slice/nested codecs. + nested, encoderTypes := collectNestedTypes() + + // Emit each top-level component type and its methods. + for _, spec := range manifest { + emitStruct(&buf, spec.Struct, spec.Comment, spec.Fields) + emitComponentMethods(&buf, spec) + emitConstants(&buf, spec) + } + + // Emit nested types (sorted for determinism). + for _, name := range sortedNestedNames(nested) { + emitStruct(&buf, name, commentsFor(nested, name), nested[name]) + } + + // Emit encode() functions only for types actually encoded via + // slice/nested codecs (avoid unused functions for NameOrTags-only types). + for _, name := range sortedNestedNames(encoderTypes) { + emitNestedEncoder(&buf, name, encoderTypes[name]) + } + + // Emit shared helper functions. + emitHelpers(&buf) + + return writeFormattedFile(filepath.Join(g.outputDir, "components_gen.go"), buf.Bytes()) +} + +// writeFormattedFile writes Go source formatted with gofmt so generated files +// are always gofmt-clean. +func writeFormattedFile(path string, src []byte) error { + formatted, err := format.Source(src) + if err != nil { + return err + } + return os.WriteFile(path, formatted, 0o644) +} + +// collectNestedTypes builds name -> fields for every nested struct referenced by +// slice/nested codecs in the manifest, including nested-of-nested. It returns the +// full set of nested types plus the subset that require an encode() function +// (those used via slice/nested codecs, not NameOrTags). +func collectNestedTypes() (map[string][]Field, map[string][]Field) { + nested := map[string][]Field{} + encoderTypes := map[string][]Field{} + var walk func([]Field, bool) + walk = func(fields []Field, needsEncoder bool) { + for _, f := range fields { + var nestedName string + childNeedsEncoder := false + switch f.Codec { + case CodecSlice, CodecNested: + nestedName = f.SliceElement + childNeedsEncoder = true + case CodecNameOrTags: + nestedName = f.SliceElement + } + if nestedName != "" && !isScalarType(nestedName) && !isBuiltin(nestedName) { + if _, ok := nested[nestedName]; !ok && len(f.Fields) > 0 { + nested[nestedName] = f.Fields + walk(f.Fields, childNeedsEncoder) + } + if childNeedsEncoder { + encoderTypes[nestedName] = f.Fields + } + } + } + } + for _, spec := range manifest { + walk(spec.Fields, false) + } + return nested, encoderTypes +} + +func sortedNestedNames(nested map[string][]Field) []string { + names := make([]string, 0, len(nested)) + for n := range nested { + names = append(names, n) + } + sortStrings(names) + return names +} + +func commentsFor(nested map[string][]Field, name string) string { + return name + " represents a nested structure used by a component." +} + +// emitStruct writes a struct declaration. +func emitStruct(buf *bytes.Buffer, name, comment string, fields []Field) { + if comment != "" { + fmt.Fprintf(buf, "// %s\n", comment) + } + fmt.Fprintf(buf, "type %s struct {\n", name) + for _, f := range fields { + fmt.Fprintf(buf, "\t%s %s\n", f.GoName, f.Type) + } + buf.WriteString("}\n\n") +} + +// emitComponentMethods writes ComponentName() and Encode() for a component spec. +func emitComponentMethods(buf *bytes.Buffer, spec Spec) { + receiver := strings.ToLower(spec.Struct[:1]) + + fmt.Fprintf(buf, "func (%s %s) ComponentName() string { return %q }\n\n", receiver, spec.Struct, spec.Name) + + fmt.Fprintf(buf, "func (%s %s) Encode() (map[string]any, error) {\n", receiver, spec.Struct) + buf.WriteString("\tdata := map[string]any{}\n") + for _, f := range spec.Fields { + emitFieldEncode(buf, receiver, f, 1) + } + buf.WriteString("\treturn data, nil\n") + buf.WriteString("}\n\n") +} + +// emitConstants writes the named constant type (if any) and the constant group. +func emitConstants(buf *bytes.Buffer, spec Spec) { + if len(spec.ConstantGroup) == 0 { + return + } + if spec.ConstantType != "" { + fmt.Fprintf(buf, "// %s is a named string type for the %q slot values.\n", spec.ConstantType, spec.Name) + fmt.Fprintf(buf, "type %s string\n\n", spec.ConstantType) + } + buf.WriteString("const (\n") + for _, c := range spec.ConstantGroup { + fmt.Fprintf(buf, "\t%s\n", c) + } + buf.WriteString(")\n\n") +} + +// emitFieldEncode writes the assignment for a single field into data. +func emitFieldEncode(buf *bytes.Buffer, receiver string, f Field, indent int) { + key := f.NBTName + if key == "" { + return + } + + if f.OmitEmpty { + fmt.Fprintf(buf, "%sif %s {\n", strings.Repeat("\t", indent), omitCondition(receiver, f)) + indent++ + } + fmt.Fprintf(buf, "%sdata[%q] = %s\n", strings.Repeat("\t", indent), key, encodeExpr(receiver, f)) + if f.OmitEmpty { + fmt.Fprintf(buf, "%s}\n", strings.Repeat("\t", indent-1)) + } +} + +// omitCondition returns the Go expression that determines whether a field is omitted. +func omitCondition(receiver string, f Field) string { + if strings.HasPrefix(f.Type, "[]") { + return "len(" + receiver + "." + f.GoName + ") > 0" + } + return receiver + "." + f.GoName + " != " + zeroValue(f.Type) +} + +// encodeExpr returns the Go expression producing the encoded value for a field. +func encodeExpr(receiver string, f Field) string { + ref := receiver + "." + f.GoName + switch f.Codec { + case CodecStringSlice: + return "stringSlice(" + ref + ")" + case CodecRange: + return "rangeData(" + ref + ")" + case CodecRangeInt: + return "rangeIntData(" + ref + ")" + case CodecList3: + return "list3Data(" + ref + ")" + case CodecBanned: + return "bannedItemsData(" + ref + ")" + case CodecScalarSlice: + return "scalarSlice(" + ref + ")" + case CodecSlice: + if f.SliceElement == "" || isScalarType(f.SliceElement) { + return "scalarSlice(" + ref + ")" + } + return "encodeSlice(" + ref + ", encode" + f.SliceElement + ")" + case CodecNested: + return "encode" + f.SliceElement + "(" + ref + ")" + case CodecNameOrTags: + return "encodeNameOrTags(" + ref + ")" + default: + return ref + } +} + +// zeroValue returns the zero value literal for the given type. +func zeroValue(t string) string { + switch { + case strings.HasPrefix(t, "["): + return "(" + t + "{})" + case strings.HasPrefix(t, "[]"): + return "nil" + case t == "string": + return "\"\"" + case t == "bool": + return "false" + default: + return "0" + } +} + +// isScalarType reports whether t is a primitive Go type. +func isScalarType(t string) bool { + switch t { + case "string", "int", "int32", "int64", "float32", "float64", "bool", "byte": + return true + } + return false +} + +// isBuiltin reports whether a type is a builtin composite (array/slice/map). +func isBuiltin(t string) bool { + return strings.HasPrefix(t, "[") || strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "map[") +} + +// emitNestedEncoder writes an encode() function. +func emitNestedEncoder(buf *bytes.Buffer, name string, fields []Field) { + fmt.Fprintf(buf, "func encode%s(v %s) map[string]any {\n", name, name) + buf.WriteString("\tdata := map[string]any{}\n") + for _, f := range fields { + emitFieldEncode(buf, "v", f, 1) + } + buf.WriteString("\treturn data\n") + buf.WriteString("}\n\n") +} + +// emitHelpers writes the transform helper functions referenced by the encoders. +func emitHelpers(buf *bytes.Buffer) { + buf.WriteString("func stringSlice(s []string) []any {\n") + buf.WriteString("\tr := make([]any, len(s))\n") + buf.WriteString("\tfor i, v := range s {\n") + buf.WriteString("\t\tr[i] = v\n") + buf.WriteString("\t}\n") + buf.WriteString("\treturn r\n") + buf.WriteString("}\n\n") + + buf.WriteString("func scalarSlice[T any](s []T) []any {\n") + buf.WriteString("\tr := make([]any, len(s))\n") + buf.WriteString("\tfor i, v := range s {\n") + buf.WriteString("\t\tr[i] = v\n") + buf.WriteString("\t}\n") + buf.WriteString("\treturn r\n") + buf.WriteString("}\n\n") + + buf.WriteString("func encodeSlice[V any](vs []V, enc func(V) map[string]any) []any {\n") + buf.WriteString("\tr := make([]any, len(vs))\n") + buf.WriteString("\tfor i, v := range vs {\n") + buf.WriteString("\t\tr[i] = enc(v)\n") + buf.WriteString("\t}\n") + buf.WriteString("\treturn r\n") + buf.WriteString("}\n\n") + + buf.WriteString("func rangeData(r [2]float32) map[string]any {\n") + buf.WriteString("\treturn map[string]any{\"min\": r[0], \"max\": r[1]}\n") + buf.WriteString("}\n\n") + + buf.WriteString("func rangeIntData(r [2]int32) map[string]any {\n") + buf.WriteString("\treturn map[string]any{\"min\": r[0], \"max\": r[1]}\n") + buf.WriteString("}\n\n") + + buf.WriteString("func list3Data(c [3]int32) []any {\n") + buf.WriteString("\treturn []any{c[0], c[1], c[2]}\n") + buf.WriteString("}\n\n") + + buf.WriteString("func bannedItemsData(items []string) []any {\n") + buf.WriteString("\tbanned := make([]any, len(items))\n") + buf.WriteString("\tfor i, b := range items {\n") + buf.WriteString("\t\tbanned[i] = map[string]any{\"name\": b}\n") + buf.WriteString("\t}\n") + buf.WriteString("\treturn banned\n") + buf.WriteString("}\n\n") + + buf.WriteString("func encodeNameOrTags(items []RepairItemEntry) []any {\n") + buf.WriteString("\tr := make([]any, len(items))\n") + buf.WriteString("\tfor i, item := range items {\n") + buf.WriteString("\t\tif item.Name != \"\" {\n") + buf.WriteString("\t\t\tr[i] = map[string]any{\"name\": item.Name}\n") + buf.WriteString("\t\t} else {\n") + buf.WriteString("\t\t\tr[i] = map[string]any{\"tags\": stringSlice(item.Tags)}\n") + buf.WriteString("\t\t}\n") + buf.WriteString("\t}\n") + buf.WriteString("\treturn r\n") + buf.WriteString("}\n\n") +} + +// sortStrings sorts a string slice in place. +func sortStrings(s []string) { + for i := 1; i < len(s); i++ { + for j := i; j > 0 && s[j-1] > s[j]; j-- { + s[j-1], s[j] = s[j], s[j-1] + } + } +} diff --git a/cmd/generate/componentgen/generate/generator.go b/cmd/generate/componentgen/generate/generator.go new file mode 100644 index 0000000000..835c3e3592 --- /dev/null +++ b/cmd/generate/componentgen/generate/generator.go @@ -0,0 +1,191 @@ +package generate + +import ( + "bytes" + "fmt" + "path/filepath" + "sort" + "strings" + "text/template" + + "github.com/df-mc/dragonfly/cmd/generate/componentgen/parse" +) + +type Generator struct { + outputDir string + schemas map[string]parse.ComponentSchema +} + +func NewGenerator(outputDir string, schemas map[string]parse.ComponentSchema) *Generator { + // Filter to only known components that we have typed implementations for + filtered := parse.FilterKnownComponents(schemas) + return &Generator{ + outputDir: outputDir, + schemas: filtered, + } +} + +func (g *Generator) Generate() error { + // Generate component type implementations (structs + ComponentName + Encode). + if err := g.generateComponentImplementations(); err != nil { + return fmt.Errorf("generate component implementations: %w", err) + } + + // Generate constants.go + if err := g.generateConstants(); err != nil { + return fmt.Errorf("generate constants: %w", err) + } + + // Generate validation.go + if err := g.generateValidation(); err != nil { + return fmt.Errorf("generate validation: %w", err) + } + + return nil +} + +func (g *Generator) generateConstants() error { + tmpl := template.Must(template.New("constants").Parse(constantsTemplate)) + + type Comp struct { + Name string + ConstName string + StructName string + } + + var components []Comp + for _, spec := range manifest { + components = append(components, Comp{ + Name: spec.Name, + ConstName: componentNameToConst(spec.Name), + StructName: spec.Struct, + }) + } + + data := map[string]any{ + "Components": components, + } + + path := filepath.Join(g.outputDir, "constants.go") + return renderAndWrite(path, tmpl, data) +} + +func (g *Generator) generateValidation() error { + tmpl := template.Must(template.New("validation").Parse(validationTemplate)) + + type Comp struct { + Name string + StructName string + } + + var components []Comp + for _, name := range sortedSchemaNames(g.schemas) { + structName := componentNameToStruct(name) + components = append(components, Comp{ + Name: name, + StructName: structName, + }) + } + + data := map[string]any{ + "Components": components, + } + + path := filepath.Join(g.outputDir, "validation.go") + return renderAndWrite(path, tmpl, data) +} + +// renderAndWrite renders tmpl with data, gofmt-formats the result and writes it +// to path so generated files are always gofmt-clean. +func renderAndWrite(path string, tmpl *template.Template, data any) error { + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return err + } + return writeFormattedFile(path, buf.Bytes()) +} + +// sortedSchemaNames returns the keys of schemas in sorted order so that generated +// files are deterministic regardless of Go map iteration order. +func sortedSchemaNames(schemas map[string]parse.ComponentSchema) []string { + names := make([]string, 0, len(schemas)) + for name := range schemas { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func componentNameToStruct(name string) string { + // Remove "minecraft:" prefix + name = strings.TrimPrefix(name, "minecraft:") + // Convert snake_case to PascalCase + parts := strings.Split(name, "_") + for i, p := range parts { + if len(p) > 0 { + parts[i] = strings.ToUpper(p[:1]) + p[1:] + } + } + return strings.Join(parts, "") +} + +func componentNameToConst(name string) string { + // Remove "minecraft:" prefix + name = strings.TrimPrefix(name, "minecraft:") + // Convert to CONSTANT_CASE + return strings.ToUpper(strings.ReplaceAll(name, ":", "_")) +} + +const constantsTemplate = `// Code generated by componentgen. DO NOT EDIT. +package component + +// Component name constants +const ( +{{- range .Components }} + Component{{ .ConstName }} = "{{ .Name }}" +{{- end }} +) +` + +const validationTemplate = `// Code generated by componentgen. DO NOT EDIT. +package component + +import "fmt" + +// ValidationRules contains cross-component validation rules derived from BDS. +var ValidationRules = map[string][]string{ + "minecraft:kinetic_weapon": {"minecraft:use_modifiers"}, + "minecraft:bundle_interaction": {"minecraft:storage_item"}, + "minecraft:shooter": {"minecraft:use_modifiers"}, +} + +// ValidateComponents checks cross-component constraints. +func ValidateComponents(components map[string]any) error { + // kinetic_weapon requires use_modifiers + if _, ok := components["minecraft:kinetic_weapon"]; ok { + if _, ok := components["minecraft:use_modifiers"]; !ok { + return fmt.Errorf("minecraft:kinetic_weapon requires minecraft:use_modifiers component") + } + } + + // bundle_interaction requires storage_item + if _, ok := components["minecraft:bundle_interaction"]; ok { + if _, ok := components["minecraft:storage_item"]; !ok { + return fmt.Errorf("minecraft:bundle_interaction requires minecraft:storage_item component") + } + } + + // shooter requires use_modifiers with non-zero use_duration + if _, ok := components["minecraft:shooter"]; ok { + if um, ok := components["minecraft:use_modifiers"].(map[string]any); ok { + if ud, ok := um["use_duration"].(float32); !ok || ud == 0 { + return fmt.Errorf("minecraft:shooter requires non-zero use_duration in minecraft:use_modifiers") + } + } else { + return fmt.Errorf("minecraft:shooter requires minecraft:use_modifiers component with non-zero use_duration") + } + } + + return nil +} +` diff --git a/cmd/generate/componentgen/generate/manifest.go b/cmd/generate/componentgen/generate/manifest.go new file mode 100644 index 0000000000..e1ff5b4514 --- /dev/null +++ b/cmd/generate/componentgen/generate/manifest.go @@ -0,0 +1,501 @@ +package generate + +// Codec describes how a component field is encoded to its NBT representation. +type Codec string + +const ( + // CodecDirect encodes the field's value directly under its NBT name. + CodecDirect Codec = "direct" + // CodecStringSlice encodes a []string as a []any. + CodecStringSlice Codec = "string_slice" + // CodecList3 encodes a [3]int32 as a []any. + CodecList3 Codec = "list3" + // CodecRange encodes a [2]float32 as a {"min","max"} compound. + CodecRange Codec = "range" + // CodecRangeInt encodes a [2]int32 as a {"min","max"} compound. + CodecRangeInt Codec = "range_int" + // CodecSlice encodes a []Nested struct as a []any of encoded compounds. + CodecSlice Codec = "slice" + // CodecScalarSlice encodes a []T where T is a scalar as a []any of values. + CodecScalarSlice Codec = "scalar_slice" + // CodecNested encodes a single Nested struct as a compound. + CodecNested Codec = "nested" + // CodecBanned encodes a []string as a []any of {"name"} compounds. + CodecBanned Codec = "banned" + // CodecNameOrTags encodes a []RepairItemEntry as {"name"} or {"tags"} compounds. + CodecNameOrTags Codec = "name_or_tags" +) + +// Field describes a single struct field of a component (or nested type). +type Field struct { + // GoName is the exported Go field name (e.g. "MaxDurability"). + GoName string + // NBTName is the wire key (e.g. "max_durability"). Empty when not emitted directly. + NBTName string + // Type is the Go field type (e.g. "int32", "[]string", "WeaponConditions"). + Type string + // Codec selects the encode behaviour. Defaults to CodecDirect. + Codec Codec + // SliceElement, when Codec is slice/nested/name_or_tags, names the nested type + // whose fields are declared in Fields. + SliceElement string + // Fields holds the nested type's fields for slice/nested/name_or_tags codecs. + Fields []Field + // OmitEmpty omits the key when the value is the zero value of its type. + OmitEmpty bool +} + +// Spec describes a single generated component type. +type Spec struct { + // Struct is the exported Go struct name (e.g. "Food"). + Struct string + // Name is the namespaced component name (e.g. "minecraft:food"). + Name string + // Comment is the doc comment for the struct. + Comment string + // Fields lists the struct fields in declaration order. + Fields []Field + // ConstantGroup is a set of package-level constants emitted alongside the struct. + ConstantGroup []string + // ConstantType, when non-empty, names a new named string type for the + // ConstantGroup constants (e.g. "SlotArmor"). The type declaration is emitted + // before the constants, which then declare `Name ConstantType = "value"`. + ConstantType string +} + +// manifest lists every component that should be generated. +var manifest = []Spec{ + { + Struct: "Wearable", + Name: "minecraft:wearable", + Comment: "Wearable represents the minecraft:wearable component.", + Fields: []Field{ + {GoName: "Slot", NBTName: "slot", Type: "SlotArmor"}, + {GoName: "Protection", NBTName: "protection", Type: "int32"}, + {GoName: "HidesPlayerLocation", NBTName: "hides_player_location", Type: "bool"}, + {GoName: "Dispensable", NBTName: "dispensable", Type: "bool"}, + }, + ConstantType: "SlotArmor", + ConstantGroup: []string{ + "SlotArmorHead SlotArmor = \"slot.armor.head\"", + "SlotArmorChest SlotArmor = \"slot.armor.chest\"", + "SlotArmorLegs SlotArmor = \"slot.armor.legs\"", + "SlotArmorFeet SlotArmor = \"slot.armor.feet\"", + }, + }, + { + Struct: "Food", + Name: "minecraft:food", + Comment: "Food represents the minecraft:food component.", + Fields: []Field{ + {GoName: "Nutrition", NBTName: "nutrition", Type: "int32"}, + {GoName: "SaturationModifier", NBTName: "saturation_modifier", Type: "float32"}, + {GoName: "CanAlwaysEat", NBTName: "can_always_eat", Type: "bool"}, + {GoName: "UsingConvertsTo", NBTName: "using_converts_to", Type: "string", OmitEmpty: true}, + {GoName: "OnUseAction", NBTName: "on_use_action", Type: "int32", OmitEmpty: true}, + {GoName: "CooldownTime", NBTName: "cooldown_time", Type: "int32", OmitEmpty: true}, + {GoName: "CooldownType", NBTName: "cooldown_type", Type: "string", OmitEmpty: true}, + {GoName: "Effects", NBTName: "effects", Type: "[]FoodEffect", Codec: CodecSlice, SliceElement: "FoodEffect", OmitEmpty: true, + Fields: []Field{ + {GoName: "ID", NBTName: "id", Type: "int32"}, + {GoName: "Duration", NBTName: "duration", Type: "int32"}, + {GoName: "Amplifier", NBTName: "amplifier", Type: "int32"}, + {GoName: "Chance", NBTName: "chance", Type: "float32"}, + {GoName: "Name", NBTName: "name", Type: "string", OmitEmpty: true}, + }}, + {GoName: "RemoveEffects", NBTName: "remove_effects", Type: "[]int32", Codec: CodecScalarSlice, OmitEmpty: true}, + }, + }, + { + Struct: "Durability", + Name: "minecraft:durability", + Comment: "Durability represents the minecraft:durability component.", + Fields: []Field{ + {GoName: "MaxDurability", NBTName: "max_durability", Type: "int32"}, + {GoName: "DamageChance", NBTName: "damage_chance", Type: "[2]int32", Codec: CodecRangeInt}, + }, + }, + { + Struct: "KineticWeapon", + Name: "minecraft:kinetic_weapon", + Comment: "KineticWeapon represents the minecraft:kinetic_weapon component.", + Fields: []Field{ + {GoName: "CreativeReach", NBTName: "creative_reach", Type: "[2]float32", Codec: CodecRange}, + {GoName: "DamageConditions", NBTName: "damage_conditions", Type: "WeaponConditions", Codec: CodecNested, SliceElement: "WeaponConditions", + Fields: []Field{ + {GoName: "MaxDuration", NBTName: "max_duration", Type: "int32"}, + {GoName: "MinSpeed", NBTName: "min_speed", Type: "float32"}, + {GoName: "MinRelativeSpeed", NBTName: "min_relative_speed", Type: "float32"}, + }}, + {GoName: "DamageModifier", NBTName: "damage_modifier", Type: "float32"}, + {GoName: "DamageMultiplier", NBTName: "damage_multiplier", Type: "float32"}, + {GoName: "Delay", NBTName: "delay", Type: "int32"}, + {GoName: "DismountConditions", NBTName: "dismount_conditions", Type: "WeaponConditions", Codec: CodecNested, SliceElement: "WeaponConditions", + Fields: []Field{ + {GoName: "MaxDuration", NBTName: "max_duration", Type: "int32"}, + {GoName: "MinSpeed", NBTName: "min_speed", Type: "float32"}, + {GoName: "MinRelativeSpeed", NBTName: "min_relative_speed", Type: "float32"}, + }}, + {GoName: "HitboxMargin", NBTName: "hitbox_margin", Type: "float32"}, + {GoName: "KnockbackConditions", NBTName: "knockback_conditions", Type: "WeaponConditions", Codec: CodecNested, SliceElement: "WeaponConditions", + Fields: []Field{ + {GoName: "MaxDuration", NBTName: "max_duration", Type: "int32"}, + {GoName: "MinSpeed", NBTName: "min_speed", Type: "float32"}, + {GoName: "MinRelativeSpeed", NBTName: "min_relative_speed", Type: "float32"}, + }}, + {GoName: "Reach", NBTName: "reach", Type: "[2]float32", Codec: CodecRange}, + }, + }, + { + Struct: "UseModifiers", + Name: "minecraft:use_modifiers", + Comment: "UseModifiers represents the minecraft:use_modifiers component.", + Fields: []Field{ + {GoName: "MovementModifier", NBTName: "movement_modifier", Type: "float32"}, + {GoName: "UseDuration", NBTName: "use_duration", Type: "float32"}, + {GoName: "EmitVibrations", NBTName: "emit_vibrations", Type: "bool"}, + {GoName: "StartSound", NBTName: "start_sound", Type: "string", OmitEmpty: true}, + {GoName: "StartUsing", NBTName: "start_using", Type: "string", OmitEmpty: true}, + }, + }, + { + Struct: "StorageItem", + Name: "minecraft:storage_item", + Comment: "StorageItem represents the minecraft:storage_item component.", + Fields: []Field{ + {GoName: "AllowNestedStorageItems", NBTName: "allow_nested_storage_items", Type: "bool"}, + {GoName: "AllowedItems", NBTName: "allowed_items", Type: "[]string", Codec: CodecStringSlice}, + {GoName: "BannedItems", NBTName: "banned_items", Type: "[]string", Codec: CodecBanned}, + {GoName: "MaxSlots", NBTName: "max_slots", Type: "int32"}, + }, + }, + { + Struct: "Shooter", + Name: "minecraft:shooter", + Comment: "Shooter represents the minecraft:shooter component.", + Fields: []Field{ + {GoName: "Ammunition", NBTName: "ammunition", Type: "[]Ammunition", Codec: CodecSlice, SliceElement: "Ammunition", + Fields: []Field{ + {GoName: "Item", NBTName: "item", Type: "string"}, + {GoName: "SearchInventory", NBTName: "search_inventory", Type: "bool"}, + {GoName: "UseInCreative", NBTName: "use_in_creative", Type: "bool"}, + {GoName: "UseOffHand", NBTName: "use_offhand", Type: "bool"}, + }}, + {GoName: "ChargeOnDraw", NBTName: "charge_on_draw", Type: "bool"}, + {GoName: "MaxDrawDuration", NBTName: "max_draw_duration", Type: "float32"}, + {GoName: "ScalePowerByDrawDuration", NBTName: "scale_power_by_draw_duration", Type: "bool"}, + }, + }, + { + Struct: "Projectile", + Name: "minecraft:projectile", + Comment: "Projectile represents the minecraft:projectile component.", + Fields: []Field{ + {GoName: "MinimumCriticalPower", NBTName: "minimum_critical_power", Type: "float32"}, + {GoName: "ProjectileEntity", NBTName: "projectile_entity", Type: "string"}, + }, + }, + { + Struct: "Throwable", + Name: "minecraft:throwable", + Comment: "Throwable represents the minecraft:throwable component.", + Fields: []Field{ + {GoName: "DoSwingAnimation", NBTName: "do_swing_animation", Type: "bool"}, + {GoName: "LaunchPowerScale", NBTName: "launch_power_scale", Type: "float32", OmitEmpty: true}, + {GoName: "MaxDrawDuration", NBTName: "max_draw_duration", Type: "float32", OmitEmpty: true}, + {GoName: "MaxLaunchPower", NBTName: "max_launch_power", Type: "float32", OmitEmpty: true}, + {GoName: "MinDrawDuration", NBTName: "min_draw_duration", Type: "float32", OmitEmpty: true}, + {GoName: "ScalePowerByDrawDuration", NBTName: "scale_power_by_draw_duration", Type: "bool", OmitEmpty: true}, + }, + }, + { + Struct: "Damage", + Name: "minecraft:damage", + Comment: "Damage represents the minecraft:damage component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "float32"}, + }, + }, + { + Struct: "Cooldown", + Name: "minecraft:cooldown", + Comment: "Cooldown represents the minecraft:cooldown component.", + Fields: []Field{ + {GoName: "Category", NBTName: "category", Type: "string"}, + {GoName: "Duration", NBTName: "duration", Type: "float32"}, + {GoName: "Type", NBTName: "type", Type: "string", OmitEmpty: true}, + }, + }, + { + Struct: "Enchantable", + Name: "minecraft:enchantable", + Comment: "Enchantable represents the minecraft:enchantable component.", + Fields: []Field{ + {GoName: "Slot", NBTName: "slot", Type: "string"}, + {GoName: "Value", NBTName: "value", Type: "int32"}, + }, + }, + { + Struct: "Repairable", + Name: "minecraft:repairable", + Comment: "Repairable represents the minecraft:repairable component.", + Fields: []Field{ + {GoName: "RepairItems", NBTName: "repair_items", Type: "[]RepairEntry", Codec: CodecSlice, SliceElement: "RepairEntry", + Fields: []Field{ + {GoName: "Items", NBTName: "items", Type: "[]RepairItemEntry", Codec: CodecNameOrTags, SliceElement: "RepairItemEntry", + Fields: []Field{ + {GoName: "Name", NBTName: "name", Type: "string"}, + {GoName: "Tags", NBTName: "tags", Type: "[]string", Codec: CodecStringSlice}, + }}, + {GoName: "RepairAmount", NBTName: "repair_amount", Type: "int32"}, + }}, + }, + }, + { + Struct: "ItemTags", + Name: "minecraft:item_tags", + Comment: "ItemTags represents the minecraft:item_tags component.", + Fields: []Field{ + {GoName: "Tags", NBTName: "tags", Type: "[]string", Codec: CodecStringSlice}, + }, + }, + { + Struct: "Tags", + Name: "minecraft:tags", + Comment: "Tags represents the minecraft:tags component.", + Fields: []Field{ + {GoName: "Tags", NBTName: "tags", Type: "[]string", Codec: CodecStringSlice}, + }, + }, + { + Struct: "Seed", + Name: "minecraft:seed", + Comment: "Seed represents the minecraft:seed component.", + Fields: []Field{ + {GoName: "CropResult", NBTName: "crop_result", Type: "string"}, + {GoName: "PlantAt", NBTName: "plant_at", Type: "[]string", Codec: CodecStringSlice}, + {GoName: "PlantAtAnySolidSurface", NBTName: "plant_at_any_solid_surface", Type: "bool"}, + {GoName: "PlantAtFace", NBTName: "plant_at_face", Type: "string"}, + }, + }, + { + Struct: "Fuel", + Name: "minecraft:fuel", + Comment: "Fuel represents the minecraft:fuel component.", + Fields: []Field{ + {GoName: "Duration", NBTName: "duration", Type: "float32"}, + }, + }, + { + Struct: "FireResistant", + Name: "minecraft:fire_resistant", + Comment: "FireResistant represents the minecraft:fire_resistant component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "bool"}, + }, + }, + { + Struct: "Glint", + Name: "minecraft:glint", + Comment: "Glint represents the minecraft:glint component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "bool"}, + }, + }, + { + Struct: "SwingDuration", + Name: "minecraft:swing_duration", + Comment: "SwingDuration represents the minecraft:swing_duration component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "float32"}, + }, + }, + { + Struct: "SwingSounds", + Name: "minecraft:swing_sounds", + Comment: "SwingSounds represents the minecraft:swing_sounds component.", + Fields: []Field{ + {GoName: "AttackCriticalHit", NBTName: "attack_critical_hit", Type: "string"}, + {GoName: "AttackHit", NBTName: "attack_hit", Type: "string"}, + {GoName: "AttackMiss", NBTName: "attack_miss", Type: "string"}, + }, + }, + { + Struct: "PiercingWeapon", + Name: "minecraft:piercing_weapon", + Comment: "PiercingWeapon represents the minecraft:piercing_weapon component.", + Fields: []Field{ + {GoName: "CreativeReach", NBTName: "creative_reach", Type: "[2]float32", Codec: CodecRange}, + {GoName: "HitboxMargin", NBTName: "hitbox_margin", Type: "float32"}, + {GoName: "Reach", NBTName: "reach", Type: "[2]float32", Codec: CodecRange}, + }, + }, + { + Struct: "Camera", + Name: "minecraft:camera", + Comment: "Camera represents the minecraft:camera component.", + Fields: []Field{ + {GoName: "BlackBarsDuration", NBTName: "black_bars_duration", Type: "float32"}, + {GoName: "BlackBarsScreenRatio", NBTName: "black_bars_screen_ratio", Type: "float32"}, + {GoName: "PictureDuration", NBTName: "picture_duration", Type: "float32"}, + {GoName: "ShutterDuration", NBTName: "shutter_duration", Type: "float32"}, + {GoName: "ShutterScreenRatio", NBTName: "shutter_screen_ratio", Type: "float32"}, + {GoName: "SlideAwayDuration", NBTName: "slide_away_duration", Type: "float32"}, + {GoName: "UseDuration", NBTName: "use_duration", Type: "int32"}, + }, + }, + { + Struct: "Block", + Name: "minecraft:block", + Comment: "Block represents the minecraft:block component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "string"}, + }, + }, + { + Struct: "BlockPlacer", + Name: "minecraft:block_placer", + Comment: "BlockPlacer represents the minecraft:block_placer component.", + Fields: []Field{ + {GoName: "Block", NBTName: "block", Type: "string"}, + {GoName: "ReplaceBlockItem", NBTName: "replace_block_item", Type: "string"}, + {GoName: "AlignedPlacement", NBTName: "aligned_placement", Type: "bool"}, + {GoName: "UseOn", NBTName: "use_on", Type: "[]string", Codec: CodecStringSlice, OmitEmpty: true}, + }, + }, + { + Struct: "Compostable", + Name: "minecraft:compostable", + Comment: "Compostable represents the minecraft:compostable component.", + Fields: []Field{ + {GoName: "CompostingChance", NBTName: "composting_chance", Type: "int32"}, + }, + }, + { + Struct: "DamageAbsorption", + Name: "minecraft:damage_absorption", + Comment: "DamageAbsorption represents the minecraft:damage_absorption component.", + Fields: []Field{ + {GoName: "AbsorbableCauses", NBTName: "absorbable_causes", Type: "[]string", Codec: CodecStringSlice}, + }, + }, + { + Struct: "Digger", + Name: "minecraft:digger", + Comment: "Digger represents the minecraft:digger component.", + Fields: []Field{ + {GoName: "DestroySpeeds", NBTName: "destroy_speeds", Type: "[]DestroySpeed", Codec: CodecSlice, SliceElement: "DestroySpeed", + Fields: []Field{ + {GoName: "Block", NBTName: "block", Type: "string"}, + {GoName: "Speed", NBTName: "speed", Type: "float32"}, + }}, + {GoName: "UseEfficiency", NBTName: "use_efficiency", Type: "bool"}, + }, + }, + { + Struct: "DurabilitySensor", + Name: "minecraft:durability_sensor", + Comment: "DurabilitySensor represents the minecraft:durability_sensor component.", + Fields: []Field{ + {GoName: "SoundEvent", NBTName: "sound_event", Type: "string", OmitEmpty: true}, + {GoName: "DurabilityThresholds", NBTName: "durability_thresholds", Type: "[]DurabilityThreshold", Codec: CodecSlice, SliceElement: "DurabilityThreshold", OmitEmpty: true, + Fields: []Field{ + {GoName: "Durability", NBTName: "durability", Type: "int32"}, + {GoName: "ParticleType", NBTName: "particle_type", Type: "string", OmitEmpty: true}, + {GoName: "SoundEvent", NBTName: "sound_event", Type: "string", OmitEmpty: true}, + }}, + }, + }, + { + Struct: "Dyeable", + Name: "minecraft:dyeable", + Comment: "Dyeable represents the minecraft:dyeable component.", + Fields: []Field{ + {GoName: "DefaultColor", NBTName: "default_color", Type: "[3]int32", Codec: CodecList3}, + }, + }, + { + Struct: "EntityPlacer", + Name: "minecraft:entity_placer", + Comment: "EntityPlacer represents the minecraft:entity_placer component.", + Fields: []Field{ + {GoName: "Entity", NBTName: "entity", Type: "string"}, + {GoName: "UseOn", NBTName: "use_on", Type: "[]string", Codec: CodecStringSlice, OmitEmpty: true}, + {GoName: "DispenseOn", NBTName: "dispense_on", Type: "[]string", Codec: CodecStringSlice, OmitEmpty: true}, + }, + }, + { + Struct: "HoverTextColor", + Name: "minecraft:hover_text_color", + Comment: "HoverTextColor represents the minecraft:hover_text_color component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "int32"}, + }, + }, + { + Struct: "InteractButton", + Name: "minecraft:interact_button", + Comment: "InteractButton represents the minecraft:interact_button component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "string"}, + }, + }, + { + Struct: "LiquidClipped", + Name: "minecraft:liquid_clipped", + Comment: "LiquidClipped represents the minecraft:liquid_clipped component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "bool"}, + }, + }, + { + Struct: "Rarity", + Name: "minecraft:rarity", + Comment: "Rarity represents the minecraft:rarity component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "string"}, + }, + }, + { + Struct: "Record", + Name: "minecraft:record", + Comment: "Record represents the minecraft:record component.", + Fields: []Field{ + {GoName: "ComparatorSignal", NBTName: "comparator_signal", Type: "int32"}, + {GoName: "Duration", NBTName: "duration", Type: "float32"}, + {GoName: "SoundEvent", NBTName: "sound_event", Type: "string"}, + }, + }, + { + Struct: "ShouldDespawn", + Name: "minecraft:should_despawn", + Comment: "ShouldDespawn represents the minecraft:should_despawn component.", + Fields: []Field{ + {GoName: "Value", NBTName: "value", Type: "bool"}, + }, + }, + { + Struct: "BundleInteraction", + Name: "minecraft:bundle_interaction", + Comment: "BundleInteraction represents the minecraft:bundle_interaction component.", + Fields: []Field{ + {GoName: "NumViewableSlots", NBTName: "num_viewable_slots", Type: "int32"}, + }, + }, + { + Struct: "StorageWeightLimit", + Name: "minecraft:storage_weight_limit", + Comment: "StorageWeightLimit represents the minecraft:storage_weight_limit component.", + Fields: []Field{ + {GoName: "MaxWeightLimit", NBTName: "max_weight_limit", Type: "int32"}, + }, + }, + { + Struct: "StorageWeightModifier", + Name: "minecraft:storage_weight_modifier", + Comment: "StorageWeightModifier represents the minecraft:storage_weight_modifier component.", + Fields: []Field{ + {GoName: "WeightInStorageItem", NBTName: "weight_in_storage_item", Type: "int32"}, + }, + }, +} diff --git a/cmd/generate/componentgen/main.go b/cmd/generate/componentgen/main.go new file mode 100644 index 0000000000..0076f5ef97 --- /dev/null +++ b/cmd/generate/componentgen/main.go @@ -0,0 +1,58 @@ +package componentgen + +import ( + "context" + "flag" + "fmt" + "path/filepath" + + "github.com/df-mc/dragonfly/cmd/generate/componentgen/generate" + "github.com/df-mc/dragonfly/cmd/generate/componentgen/parse" + "github.com/df-mc/dragonfly/cmd/generate/generator" +) + +// Generator implements the generate.Generator interface for component generation. +type Generator struct { + outputDir string + vanillaNBT string +} + +func init() { + generator.Register(&Generator{}) +} + +func (g *Generator) Name() string { + return "componentgen" +} + +func (g *Generator) SetFlags(fs *flag.FlagSet) { + fs.StringVar(&g.outputDir, "output", "", "Output directory for generated files") + fs.StringVar(&g.vanillaNBT, "vanilla-nbt", "", "Path to vanilla_items.nbt") +} + +func (g *Generator) Generate(ctx context.Context) error { + if g.outputDir == "" { + g.outputDir = filepath.Join("server", "item", "component") + } + if g.vanillaNBT == "" { + g.vanillaNBT = filepath.Join("server", "world", "vanilla_items.nbt") + } + + // Parse vanilla items NBT + items, err := parse.VanillaItems(g.vanillaNBT) + if err != nil { + return fmt.Errorf("failed to parse vanilla items: %w", err) + } + + // Extract component schemas + schemas := parse.ExtractComponentSchemas(items) + + // Generate files + gen := generate.NewGenerator(g.outputDir, schemas) + if err := gen.Generate(); err != nil { + return fmt.Errorf("generation failed: %w", err) + } + + fmt.Println("Component generation complete") + return nil +} diff --git a/cmd/generate/componentgen/parse/schemas.go b/cmd/generate/componentgen/parse/schemas.go new file mode 100644 index 0000000000..f21a06d4d3 --- /dev/null +++ b/cmd/generate/componentgen/parse/schemas.go @@ -0,0 +1,122 @@ +package parse + +// ComponentSchema represents the schema of a component extracted from vanilla items. +type ComponentSchema struct { + Name string + SampleData map[string]any + ItemCount int +} + +// ExtractComponentSchemas extracts all unique component schemas from vanilla items. +func ExtractComponentSchemas(items map[string]VanillaItemEntry) map[string]ComponentSchema { + schemas := make(map[string]ComponentSchema) + + for _, item := range items { + if item.Data == nil { + continue + } + components, ok := item.Data["components"].(map[string]any) + if !ok { + continue + } + for compName, compData := range components { + if existing, ok := schemas[compName]; ok { + existing.ItemCount++ + schemas[compName] = existing + } else { + dataMap, _ := compData.(map[string]any) + schemas[compName] = ComponentSchema{ + Name: compName, + SampleData: dataMap, + ItemCount: 1, + } + } + } + } + + return schemas +} + +// AllComponentNames returns a sorted list of all component names. +func AllComponentNames(schemas map[string]ComponentSchema) []string { + names := make([]string, 0, len(schemas)) + for name := range schemas { + names = append(names, name) + } + // Simple sort + for i := 0; i < len(names); i++ { + for j := i + 1; j < len(names); j++ { + if names[i] > names[j] { + names[i], names[j] = names[j], names[i] + } + } + } + return names +} + +// ComponentSchemasToMap converts schemas to a map for template use. +func ComponentSchemasToMap(schemas map[string]ComponentSchema) map[string]any { + result := make(map[string]any) + for name, schema := range schemas { + result[name] = map[string]any{ + "name": schema.Name, + "sample_data": schema.SampleData, + "item_count": schema.ItemCount, + } + } + return result +} + +// FilterKnownComponents filters to only known components that we have typed implementations for. +func FilterKnownComponents(schemas map[string]ComponentSchema) map[string]ComponentSchema { + known := map[string]bool{ + "minecraft:wearable": true, + "minecraft:food": true, + "minecraft:durability": true, + "minecraft:kinetic_weapon": true, + "minecraft:use_modifiers": true, + "minecraft:storage_item": true, + "minecraft:shooter": true, + "minecraft:projectile": true, + "minecraft:throwable": true, + "minecraft:damage": true, + "minecraft:cooldown": true, + "minecraft:enchantable": true, + "minecraft:repairable": true, + "minecraft:item_tags": true, + "minecraft:tags": true, + "minecraft:seed": true, + "minecraft:fuel": true, + "minecraft:fire_resistant": true, + "minecraft:glint": true, + "minecraft:swing_duration": true, + "minecraft:swing_sounds": true, + "minecraft:piercing_weapon": true, + "minecraft:camera": true, + "minecraft:block": true, + "minecraft:block_placer": true, + "minecraft:compostable": true, + "minecraft:damage_absorption": true, + "minecraft:digger": true, + "minecraft:durability_sensor": true, + "minecraft:dyeable": true, + "minecraft:entity_placer": true, + "minecraft:hover_text_color": true, + "minecraft:interact_button": true, + "minecraft:liquid_clipped": true, + "minecraft:rarity": true, + "minecraft:record": true, + "minecraft:should_despawn": true, + "minecraft:bundle_interaction": true, + "minecraft:storage_weight_limit": true, + "minecraft:storage_weight_modifier": true, + } + + result := make(map[string]ComponentSchema) + for name, schema := range schemas { + if known[name] { + result[name] = schema + } + } + return result +} diff --git a/cmd/generate/componentgen/parse/vanilla.go b/cmd/generate/componentgen/parse/vanilla.go new file mode 100644 index 0000000000..81911c6df5 --- /dev/null +++ b/cmd/generate/componentgen/parse/vanilla.go @@ -0,0 +1,29 @@ +package parse + +import ( + "os" + + "github.com/sandertv/gophertunnel/minecraft/nbt" +) + +type VanillaItemEntry struct { + RuntimeID int32 `nbt:"runtime_id"` + ComponentBased bool `nbt:"component_based"` + Version int32 `nbt:"version"` + Data map[string]any `nbt:"data,omitempty"` +} + +// VanillaItems parses the vanilla_items.nbt file and returns a map of item name to entry. +func VanillaItems(path string) (map[string]VanillaItemEntry, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var items map[string]VanillaItemEntry + if err := nbt.Unmarshal(data, &items); err != nil { + return nil, err + } + + return items, nil +} diff --git a/cmd/generate/generator/runner.go b/cmd/generate/generator/runner.go new file mode 100644 index 0000000000..c4df168c28 --- /dev/null +++ b/cmd/generate/generator/runner.go @@ -0,0 +1,89 @@ +package generator + +import ( + "context" + "flag" + "fmt" + "os" +) + +// Generator is the interface that all generators must implement. +type Generator interface { + // Name returns the name of the generator. + Name() string + // Generate runs the generator. + Generate(ctx context.Context) error + // SetFlags sets the flags for the generator. + SetFlags(*flag.FlagSet) +} + +// Registry holds all registered generators. +var Registry = map[string]Generator{} + +// Register registers a generator. +func Register(g Generator) { + if _, ok := Registry[g.Name()]; ok { + panic(fmt.Sprintf("generator %q already registered", g.Name())) + } + Registry[g.Name()] = g +} + +// Run runs the specified generators. If names is empty, runs all generators. +func Run(ctx context.Context, names ...string) error { + var generators []Generator + if len(names) == 0 { + for _, g := range Registry { + generators = append(generators, g) + } + } else { + for _, name := range names { + g, ok := Registry[name] + if !ok { + return fmt.Errorf("unknown generator %q", name) + } + generators = append(generators, g) + } + } + + // Set up flags for each generator + fs := flag.NewFlagSet("generate", flag.ContinueOnError) + fs.SetOutput(os.Stdout) + for _, g := range generators { + g.SetFlags(fs) + } + // Parse all remaining args after the generator name + // names[0] is the generator name, so we need to skip it + // We'll parse from the full os.Args but skip the first two elements (binary name + generator name) + fs.Parse(os.Args[2:]) + + for _, g := range generators { + fmt.Printf("Running generator: %s\n", g.Name()) + if err := g.Generate(ctx); err != nil { + return fmt.Errorf("generator %s failed: %w", g.Name(), err) + } + } + return nil +} + +// Main is the entry point for the generate command. +// It can be called from a go:generate directive. +func Main() { + ctx := context.Background() + + // Get generator name from first arg + args := os.Args[1:] + if len(args) == 0 { + // Run all generators + if err := Run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err) + os.Exit(1) + } + return + } + + // Run specific generator + if err := Run(ctx, args[0]); err != nil { + fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/generate/main.go b/cmd/generate/main.go new file mode 100644 index 0000000000..35368fdf9e --- /dev/null +++ b/cmd/generate/main.go @@ -0,0 +1,10 @@ +package main + +import ( + _ "github.com/df-mc/dragonfly/cmd/generate/componentgen" + "github.com/df-mc/dragonfly/cmd/generate/generator" +) + +func main() { + generator.Main() +} diff --git a/go.mod b/go.mod index f9ec0fdc10..132e5dd39b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/sandertv/gophertunnel v1.59.0 github.com/segmentio/fasthash v1.0.3 + github.com/stretchr/testify v1.11.1 golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 golang.org/x/mod v0.32.0 golang.org/x/text v0.34.0 @@ -21,6 +22,7 @@ require ( require ( github.com/coder/websocket v1.8.14 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/df-mc/go-nethernet v1.0.20 // indirect github.com/df-mc/go-playfab/v2 v2.0.2 // indirect github.com/df-mc/go-xsapi/v2 v2.0.3 // indirect @@ -45,6 +47,7 @@ require ( github.com/pion/transport/v4 v4.0.2 // indirect github.com/pion/turn/v5 v5.0.10 // indirect github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sandertv/go-raknet v1.15.2-0.20260705184311-0d1fd09e2cf6 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.48.0 // indirect @@ -54,4 +57,5 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b86e309292..ed044c50fb 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,10 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -124,6 +128,8 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index 86d1b17479..2509cfaf51 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/component" "github.com/df-mc/dragonfly/server/world" ) @@ -14,11 +15,29 @@ func Components(it world.CustomItem) (map[string]any, error) { category := it.Category() identifier, _ := it.EncodeItem() - parts := strings.SplitN(identifier, ":", 1) - if len(parts) < 2 { + _, name, ok := strings.Cut(identifier, ":") + if !ok { return nil, fmt.Errorf("identifier %s must contain namespace", identifier) } - name := parts[1] + + // Check for new ComponentItem interface first + if ci, ok := it.(component.ComponentItem); ok { + comps, err := componentsFromComponentItem(ci) + if err != nil { + return nil, err + } + builder := NewComponentBuilder(it.Name(), identifier, category) + // Add the components from ComponentItem + for name, data := range comps { + builder.AddComponent(name, data) + } + result := builder.Construct() + components := result["components"].(map[string]any) + if err := ValidateComponents(components); err != nil { + return nil, fmt.Errorf("component validation failed for %s: %w", name, err) + } + return result, nil + } builder := NewComponentBuilder(it.Name(), identifier, category) @@ -34,11 +53,11 @@ func Components(it world.CustomItem) (map[string]any, error) { case item.BootsType: slot = "slot.armor.feet" } - builder.AddComponent("wearable", map[string]any{ + builder.AddComponent("minecraft:wearable", map[string]any{ "slot": slot, "protection": int32(x.DefencePoints()), - "hides_player_location": x.HidesPlayerLocation(), - "dispensable": x.Dispensable(), + "hides_player_location": false, + "dispensable": false, }) } if x, ok := it.(item.Consumable); ok { @@ -83,7 +102,7 @@ func Components(it world.CustomItem) (map[string]any, error) { food["remove_effects"] = removeEffects } } - builder.AddComponent("food", food) + builder.AddComponent("minecraft:food", food) builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) if y, ok := it.(item.Drinkable); ok && y.Drinkable() { @@ -100,7 +119,7 @@ func Components(it world.CustomItem) (map[string]any, error) { if y, ok := it.(item.CooldownTyped); ok { cooldown["type"] = y.CooldownType() } - builder.AddComponent("cooldown", cooldown) + builder.AddComponent("minecraft:cooldown", cooldown) } if x, ok := it.(item.Durable); ok { info := x.DurabilityInfo() @@ -112,7 +131,7 @@ func Components(it world.CustomItem) (map[string]any, error) { damageChance["min"] = int32(info.DamageChance[0]) damageChance["max"] = int32(info.DamageChance[1]) } - builder.AddComponent("durability", map[string]any{ + builder.AddComponent("minecraft:durability", map[string]any{ "max_durability": int32(info.MaxDurability), "damage_chance": damageChance, }) @@ -145,12 +164,12 @@ func Components(it world.CustomItem) (map[string]any, error) { } if x, ok := it.(item.Projectile); ok { info := x.ProjectileInfo() - builder.AddComponent("projectile", map[string]any{ + builder.AddComponent("minecraft:projectile", map[string]any{ "minimum_critical_power": float32(info.MinimumCriticalPower), "projectile_entity": info.ProjectileEntity, }) } else if _, ok := it.(item.Throwable); ok { - builder.AddComponent("projectile", map[string]any{}) + builder.AddComponent("minecraft:projectile", map[string]any{}) } if x, ok := it.(item.Throwable); ok { info := x.ThrowableInfo() @@ -172,10 +191,10 @@ func Components(it world.CustomItem) (map[string]any, error) { if info.ScalePowerByDrawDuration { throwable["scale_power_by_draw_duration"] = true } - builder.AddComponent("throwable", throwable) + builder.AddComponent("minecraft:throwable", throwable) } if x, ok := it.(item.Glinted); ok { - builder.AddComponent("glint", map[string]any{ + builder.AddComponent("minecraft:glint", map[string]any{ "value": x.Glinted(), }) } @@ -183,40 +202,40 @@ func Components(it world.CustomItem) (map[string]any, error) { builder.AddProperty("hand_equipped", x.HandEquipped()) } if x, ok := it.(item.Weapon); ok { - builder.AddComponent("damage", map[string]any{ + builder.AddComponent("minecraft:damage", map[string]any{ "value": x.AttackDamage(), }) } if x, ok := it.(item.Fuel); ok { - builder.AddComponent("fuel", map[string]any{ + builder.AddComponent("minecraft:fuel", map[string]any{ "duration": float32(x.FuelInfo().Duration.Seconds()), }) } if x, ok := it.(item.FireResistant); ok { - builder.AddComponent("fire_resistant", map[string]any{ + builder.AddComponent("minecraft:fire_resistant", map[string]any{ "value": x.FireResistant(), }) } if x, ok := it.(item.EnchantableData); ok { info := x.EnchantableData() - builder.AddComponent("enchantable", map[string]any{ + builder.AddComponent("minecraft:enchantable", map[string]any{ "slot": info.Slot, "value": info.Value, }) } if x, ok := it.(item.RepairMaterials); ok { - builder.AddComponent("repairable", map[string]any{ + builder.AddComponent("minecraft:repairable", map[string]any{ "repair_items": repairItems(x.RepairMaterials()), }) } if x, ok := it.(item.Tagged); ok { tags := stringSlice(x.Tags()) - builder.AddComponent("item_tags", tags) - builder.AddComponent("tags", map[string]any{"tags": tags}) + builder.AddComponent("minecraft:item_tags", tags) + builder.AddComponent("minecraft:tags", map[string]any{"tags": tags}) } if x, ok := it.(item.Seed); ok { info := x.SeedInfo() - builder.AddComponent("seed", map[string]any{ + builder.AddComponent("minecraft:seed", map[string]any{ "crop_result": info.CropResult, "plant_at": stringSlice(info.PlantAt), "plant_at_any_solid_surface": info.PlantAtAnySolidSurface, @@ -229,12 +248,12 @@ func Components(it world.CustomItem) (map[string]any, error) { if info.NumViewableSlots < 1 || info.NumViewableSlots > 64 { return nil, fmt.Errorf("NumViewableSlots %d out of range 1-64", info.NumViewableSlots) } - builder.AddComponent("bundle_interaction", map[string]any{ + builder.AddComponent("minecraft:bundle_interaction", map[string]any{ "num_viewable_slots": int32(info.NumViewableSlots), }) } if info.MaxSlots != 0 || len(info.AllowedItems) != 0 || len(info.BannedItems) != 0 { - builder.AddComponent("storage_item", map[string]any{ + builder.AddComponent("minecraft:storage_item", map[string]any{ "allow_nested_storage_items": info.AllowNestedStorageItems, "allowed_items": stringSlice(info.AllowedItems), "banned_items": bannedItems(info.BannedItems), @@ -242,12 +261,12 @@ func Components(it world.CustomItem) (map[string]any, error) { }) } if info.MaxWeightLimit != 0 { - builder.AddComponent("storage_weight_limit", map[string]any{ + builder.AddComponent("minecraft:storage_weight_limit", map[string]any{ "max_weight_limit": int32(info.MaxWeightLimit), }) } if info.WeightInStorageItem != 0 { - builder.AddComponent("storage_weight_modifier", map[string]any{ + builder.AddComponent("minecraft:storage_weight_modifier", map[string]any{ "weight_in_storage_item": int32(info.WeightInStorageItem), }) } @@ -265,29 +284,27 @@ func Components(it world.CustomItem) (map[string]any, error) { if info.StartUsing != "" { modifiers["start_using"] = info.StartUsing } - builder.AddComponent("use_modifiers", modifiers) + builder.AddComponent("minecraft:use_modifiers", modifiers) } if x, ok := it.(item.SwingDuration); ok { - builder.AddComponent("swing_duration", map[string]any{ + builder.AddComponent("minecraft:swing_duration", map[string]any{ "value": float32(x.SwingDuration()), }) } if x, ok := it.(item.SwingSounds); ok { info := x.SwingSounds() - builder.AddComponent("swing_sounds", map[string]any{ + builder.AddComponent("minecraft:swing_sounds", map[string]any{ "attack_critical_hit": info.AttackCriticalHit, "attack_hit": info.AttackHit, "attack_miss": info.AttackMiss, }) } if x, ok := it.(item.KineticWeapon); ok { - builder.AddComponent("kinetic_weapon", map[string]any{ - "kinetic_weapon": kineticWeaponData(x.KineticWeaponInfo()), - }) + builder.AddComponent("minecraft:kinetic_weapon", kineticWeaponData(x.KineticWeaponInfo())) } if x, ok := it.(item.PiercingWeapon); ok { info := x.PiercingWeaponInfo() - builder.AddComponent("piercing_weapon", map[string]any{ + builder.AddComponent("minecraft:piercing_weapon", map[string]any{ "creative_reach": rangeData(info.CreativeReach), "hitbox_margin": float32(info.HitboxMargin), "reach": rangeData(info.Reach), @@ -295,7 +312,7 @@ func Components(it world.CustomItem) (map[string]any, error) { } if x, ok := it.(item.Camera); ok { info := x.CameraInfo() - builder.AddComponent("camera", map[string]any{ + builder.AddComponent("minecraft:camera", map[string]any{ "black_bars_duration": float32(info.BlackBarsDuration), "black_bars_screen_ratio": float32(info.BlackBarsScreenRatio), "picture_duration": float32(info.PictureDuration), @@ -303,7 +320,7 @@ func Components(it world.CustomItem) (map[string]any, error) { "shutter_screen_ratio": float32(info.ShutterScreenRatio), "slide_away_duration": float32(info.SlideAwayDuration), }) - builder.AddComponent("block", "camera") + builder.AddComponent("minecraft:block", "camera") if info.UseDuration != 0 { builder.AddProperty("use_duration", int32(info.UseDuration)) } @@ -318,15 +335,15 @@ func Components(it world.CustomItem) (map[string]any, error) { if len(info.UseOn) != 0 { blockPlacer["use_on"] = stringSlice(info.UseOn) } - builder.AddComponent("block_placer", blockPlacer) + builder.AddComponent("minecraft:block_placer", blockPlacer) } if x, ok := it.(item.Compostable); ok { - builder.AddComponent("compostable", map[string]any{ + builder.AddComponent("minecraft:compostable", map[string]any{ "composting_chance": int32(x.CompostChance() * 100), }) } if x, ok := it.(item.DamageAbsorption); ok { - builder.AddComponent("damage_absorption", map[string]any{ + builder.AddComponent("minecraft:damage_absorption", map[string]any{ "absorbable_causes": stringSlice(x.AbsorbableCauses()), }) } @@ -339,7 +356,7 @@ func Components(it world.CustomItem) (map[string]any, error) { "speed": float32(ds.Speed), }) } - builder.AddComponent("digger", map[string]any{ + builder.AddComponent("minecraft:digger", map[string]any{ "destroy_speeds": speeds, "use_efficiency": info.UseEfficiency, }) @@ -353,11 +370,11 @@ func Components(it world.CustomItem) (map[string]any, error) { if len(info.DurabilityThresholds) != 0 { sensor["durability_thresholds"] = durabilityThresholds(info.DurabilityThresholds) } - builder.AddComponent("durability_sensor", sensor) + builder.AddComponent("minecraft:durability_sensor", sensor) } if x, ok := it.(item.Dyeable); ok { c := x.DefaultColor() - builder.AddComponent("dyeable", map[string]any{ + builder.AddComponent("minecraft:dyeable", map[string]any{ "default_color": []any{int32(c[0]), int32(c[1]), int32(c[2])}, }) } @@ -372,31 +389,31 @@ func Components(it world.CustomItem) (map[string]any, error) { if len(info.DispenseOn) != 0 { entityPlacer["dispense_on"] = stringSlice(info.DispenseOn) } - builder.AddComponent("entity_placer", entityPlacer) + builder.AddComponent("minecraft:entity_placer", entityPlacer) } if x, ok := it.(item.HoverTextColor); ok { - builder.AddComponent("hover_text_color", map[string]any{ + builder.AddComponent("minecraft:hover_text_color", map[string]any{ "value": x.HoverTextColor(), }) } if x, ok := it.(item.InteractButton); ok { - builder.AddComponent("interact_button", map[string]any{ + builder.AddComponent("minecraft:interact_button", map[string]any{ "value": x.InteractButton(), }) } if x, ok := it.(item.LiquidClipped); ok { - builder.AddComponent("liquid_clipped", map[string]any{ + builder.AddComponent("minecraft:liquid_clipped", map[string]any{ "value": x.LiquidClipped(), }) } if x, ok := it.(item.Rarity); ok { - builder.AddComponent("rarity", map[string]any{ + builder.AddComponent("minecraft:rarity", map[string]any{ "value": x.Rarity(), }) } if x, ok := it.(item.Record); ok { info := x.RecordInfo() - builder.AddComponent("record", map[string]any{ + builder.AddComponent("minecraft:record", map[string]any{ "comparator_signal": int32(info.ComparatorSignal), "duration": float32(info.Duration), "sound_event": info.SoundEvent, @@ -404,7 +421,7 @@ func Components(it world.CustomItem) (map[string]any, error) { } if x, ok := it.(item.Shooter); ok { info := x.ShooterInfo() - builder.AddComponent("shooter", map[string]any{ + builder.AddComponent("minecraft:shooter", map[string]any{ "ammunition": ammunition(info.Ammunition), "charge_on_draw": info.ChargeOnDraw, "max_draw_duration": float32(info.MaxDrawDuration), @@ -412,11 +429,16 @@ func Components(it world.CustomItem) (map[string]any, error) { }) } if x, ok := it.(item.ShouldDespawn); ok { - builder.AddComponent("should_despawn", map[string]any{ + builder.AddComponent("minecraft:should_despawn", map[string]any{ "value": x.ShouldDespawn(), }) } - return builder.Construct(), nil + result := builder.Construct() + components := result["components"].(map[string]any) + if err := ValidateComponents(components); err != nil { + return nil, fmt.Errorf("component validation failed for %s: %w", name, err) + } + return result, nil } // ammunition converts a slice of ammunition to the data required for the shooter component. @@ -520,3 +542,16 @@ func durabilityThresholds(thresholds []item.DurabilityThreshold) []any { } return t } + +// componentsFromComponentItem converts a ComponentItem to a components map. +func componentsFromComponentItem(ci component.ComponentItem) (map[string]any, error) { + components := make(map[string]any) + for _, comp := range ci.ItemComponents() { + data, err := comp.Encode() + if err != nil { + return nil, err + } + components[comp.ComponentName()] = data + } + return components, nil +} diff --git a/server/internal/iteminternal/golden_test.go b/server/internal/iteminternal/golden_test.go new file mode 100644 index 0000000000..611457988d --- /dev/null +++ b/server/internal/iteminternal/golden_test.go @@ -0,0 +1,82 @@ +package iteminternal + +import ( + "encoding/json" + "os" + "testing" + + "github.com/sandertv/gophertunnel/minecraft/nbt" + "github.com/stretchr/testify/require" +) + +func TestGoldenNBT(t *testing.T) { + var vanillaItems map[string]VanillaItemEntry + _ = nbt.Unmarshal(VanillaItemsData(), &vanillaItems) + + // Test a few key items that use the new component system + testItems := []string{ + "minecraft:diamond_sword", + "minecraft:diamond_spear", + "minecraft:bow", + "minecraft:crossbow", + "minecraft:bundle", + "minecraft:shield", + } + + for _, name := range testItems { + t.Run(name, func(t *testing.T) { + item, ok := vanillaItems[name] + require.True(t, ok, "item %s not found in vanilla items", name) + + if item.Data == nil { + t.Skip("item has no component data") + } + + components, ok := item.Data["components"].(map[string]any) + if !ok { + t.Skipf("item %s has no components map", name) + } + + // Verify key components are present + expectedComponents := expectedComponentsForItem(name) + for _, compName := range expectedComponents { + require.Contains(t, components, compName, "item %s missing component %s", name, compName) + } + + // Log the component structure for debugging + b, _ := json.MarshalIndent(components, "", " ") + t.Logf("Components for %s:\n%s", name, string(b)) + }) + } +} + +type VanillaItemEntry struct { + RuntimeID int32 `nbt:"runtime_id"` + ComponentBased bool `nbt:"component_based"` + Version int32 `nbt:"version"` + Data map[string]any `nbt:"data,omitempty"` +} + +func expectedComponentsForItem(name string) []string { + switch name { + case "minecraft:diamond_sword": + return []string{"minecraft:damage", "minecraft:durability", "minecraft:enchantable", "minecraft:repairable"} + case "minecraft:diamond_spear": + return []string{"minecraft:kinetic_weapon", "minecraft:use_modifiers", "minecraft:damage", "minecraft:durability", "minecraft:enchantable", "minecraft:repairable"} + case "minecraft:bow": + return []string{"minecraft:projectile", "minecraft:durability", "minecraft:enchantable", "minecraft:repairable"} + case "minecraft:crossbow": + return []string{"minecraft:projectile", "minecraft:durability", "minecraft:enchantable", "minecraft:repairable", "minecraft:cooldown"} + case "minecraft:bundle": + return []string{"minecraft:bundle_interaction", "minecraft:storage_item"} + case "minecraft:shield": + return []string{"minecraft:durability", "minecraft:enchantable", "minecraft:repairable"} + default: + return nil + } +} + +func VanillaItemsData() []byte { + data, _ := os.ReadFile("../../../server/world/vanilla_items.nbt") + return data +} diff --git a/server/internal/iteminternal/validation_test.go b/server/internal/iteminternal/validation_test.go new file mode 100644 index 0000000000..942fc9dae5 --- /dev/null +++ b/server/internal/iteminternal/validation_test.go @@ -0,0 +1,164 @@ +package iteminternal + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidation(t *testing.T) { + tests := []struct { + name string + components map[string]any + wantErr bool + errMsg string + }{ + { + name: "kinetic_weapon without use_modifiers", + components: map[string]any{ + "minecraft:kinetic_weapon": map[string]any{ + "damage_conditions": map[string]any{"max_duration": int32(100)}, + }, + }, + wantErr: true, + errMsg: "minecraft:kinetic_weapon requires minecraft:use_modifiers component", + }, + { + name: "kinetic_weapon with use_modifiers", + components: map[string]any{ + "minecraft:kinetic_weapon": map[string]any{ + "damage_conditions": map[string]any{"max_duration": int32(100)}, + }, + "minecraft:use_modifiers": map[string]any{ + "use_duration": float32(1.0), + "movement_modifier": float32(0.2), + }, + }, + wantErr: false, + }, + { + name: "kinetic_weapon with invalid conditions (both min_speed and min_relative_speed)", + components: map[string]any{ + "minecraft:kinetic_weapon": map[string]any{ + "damage_conditions": map[string]any{ + "max_duration": int32(100), + "min_speed": float32(0.1), + "min_relative_speed": float32(0.2), + }, + }, + "minecraft:use_modifiers": map[string]any{ + "use_duration": float32(1.0), + }, + }, + wantErr: true, + errMsg: "min_speed and min_relative_speed are mutually exclusive", + }, + { + name: "kinetic_weapon with zero max_duration", + components: map[string]any{ + "minecraft:kinetic_weapon": map[string]any{ + "damage_conditions": map[string]any{ + "max_duration": int32(0), + }, + }, + "minecraft:use_modifiers": map[string]any{ + "use_duration": float32(1.0), + }, + }, + wantErr: true, + errMsg: "at least one condition with max_duration > 0 required", + }, + { + name: "bundle_interaction without storage_item", + components: map[string]any{ + "minecraft:bundle_interaction": map[string]any{ + "num_viewable_slots": int32(12), + }, + }, + wantErr: true, + errMsg: "minecraft:bundle_interaction requires minecraft:storage_item component", + }, + { + name: "bundle_interaction with storage_item", + components: map[string]any{ + "minecraft:bundle_interaction": map[string]any{ + "num_viewable_slots": int32(12), + }, + "minecraft:storage_item": map[string]any{ + "max_slots": int32(27), + }, + }, + wantErr: false, + }, + { + name: "shooter without use_modifiers", + components: map[string]any{ + "minecraft:shooter": map[string]any{ + "ammunition": []any{}, + }, + }, + wantErr: true, + errMsg: "minecraft:shooter requires minecraft:use_modifiers component with non-zero use_duration", + }, + { + name: "shooter with use_modifiers but zero use_duration", + components: map[string]any{ + "minecraft:shooter": map[string]any{ + "ammunition": []any{}, + }, + "minecraft:use_modifiers": map[string]any{ + "use_duration": float32(0), + }, + }, + wantErr: true, + errMsg: "minecraft:shooter requires non-zero use_duration in minecraft:use_modifiers", + }, + { + name: "shooter with valid use_modifiers", + components: map[string]any{ + "minecraft:shooter": map[string]any{ + "ammunition": []any{}, + }, + "minecraft:use_modifiers": map[string]any{ + "use_duration": float32(1.0), + }, + }, + wantErr: false, + }, + { + name: "repairable with invalid repair_items", + components: map[string]any{ + "minecraft:repairable": map[string]any{ + "repair_items": []any{ + map[string]any{"invalid": "entry"}, + }, + }, + }, + wantErr: true, + errMsg: "minecraft:repairable repair_items must have 'items' field", + }, + { + name: "repairable with valid repair_items", + components: map[string]any{ + "minecraft:repairable": map[string]any{ + "repair_items": []any{ + map[string]any{"items": []any{map[string]any{"name": "minecraft:diamond"}}, "repair_amount": int32(100)}, + }, + }, + }, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := ValidateComponents(tc.components) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/server/internal/iteminternal/validator.go b/server/internal/iteminternal/validator.go new file mode 100644 index 0000000000..98066545f5 --- /dev/null +++ b/server/internal/iteminternal/validator.go @@ -0,0 +1,80 @@ +package iteminternal + +import ( + "fmt" +) + +// ValidateComponents checks cross-component constraints derived from BDS 1.26.40.8 +func ValidateComponents(components map[string]any) error { + // kinetic_weapon requires use_modifiers + if _, ok := components["minecraft:kinetic_weapon"]; ok { + if _, ok := components["minecraft:use_modifiers"]; !ok { + return fmt.Errorf("minecraft:kinetic_weapon requires minecraft:use_modifiers component") + } + // Validate kinetic_weapon conditions + if err := validateKineticWeapon(components["minecraft:kinetic_weapon"]); err != nil { + return err + } + } + + // bundle_interaction requires storage_item + if _, ok := components["minecraft:bundle_interaction"]; ok { + if _, ok := components["minecraft:storage_item"]; !ok { + return fmt.Errorf("minecraft:bundle_interaction requires minecraft:storage_item component") + } + } + + // shooter requires non-zero use_duration (checked via use_modifiers component) + if _, ok := components["minecraft:shooter"]; ok { + if um, ok := components["minecraft:use_modifiers"].(map[string]any); ok { + if ud, ok := um["use_duration"].(float32); !ok || ud == 0 { + return fmt.Errorf("minecraft:shooter requires non-zero use_duration in minecraft:use_modifiers") + } + } else { + return fmt.Errorf("minecraft:shooter requires minecraft:use_modifiers component with non-zero use_duration") + } + } + + // repairable validation: repair_items must be structured entries + if rep, ok := components["minecraft:repairable"].(map[string]any); ok { + if items, ok := rep["repair_items"].([]any); ok { + for _, item := range items { + if entry, ok := item.(map[string]any); ok { + if _, hasItems := entry["items"]; !hasItems { + return fmt.Errorf("minecraft:repairable repair_items must have 'items' field") + } + } + } + } + } + + return nil +} + +func validateKineticWeapon(kw any) error { + data, ok := kw.(map[string]any) + if !ok { + return fmt.Errorf("kinetic_weapon must be a compound") + } + + // At least one kinetic condition must be defined with max_duration > 0 + hasCondition := false + for _, key := range []string{"damage_conditions", "knockback_conditions", "dismount_conditions"} { + if cond, ok := data[key].(map[string]any); ok { + if maxDur, ok := cond["max_duration"].(int32); ok && maxDur > 0 { + hasCondition = true + // min_speed and min_relative_speed are mutually exclusive + _, hasMinSpeed := cond["min_speed"] + _, hasMinRelSpeed := cond["min_relative_speed"] + if hasMinSpeed && hasMinRelSpeed { + return fmt.Errorf("kinetic_weapon %s: min_speed and min_relative_speed are mutually exclusive", key) + } + } + } + } + if !hasCondition { + return fmt.Errorf("kinetic_weapon: at least one condition with max_duration > 0 required") + } + + return nil +} diff --git a/server/internal/nbtconv/read.go b/server/internal/nbtconv/read.go index 965020b2ae..617509d856 100644 --- a/server/internal/nbtconv/read.go +++ b/server/internal/nbtconv/read.go @@ -38,7 +38,7 @@ func Int32(m map[string]any, k string) int32 { return v } -// Int64 reads an int16 value from a map at key k. +// Int64 reads an int64 value from a map at key k. func Int64(m map[string]any, k string) int64 { v, _ := m[k].(int64) return v diff --git a/server/item/armour.go b/server/item/armour.go index 148b2f8146..17f44a5392 100644 --- a/server/item/armour.go +++ b/server/item/armour.go @@ -16,12 +16,6 @@ type ( // resisted upon being attacked. 1 knock back resistance point client-side translates to 10% knock back // reduction. KnockBackResistance() float64 - // HidesPlayerLocation returns a boolean that determines whether the Player's location is hidden on Locator Maps - // and the Locator Bar when the wearable item is worn. Default is false. - HidesPlayerLocation() bool - // Dispensable returns a boolean that determines whether the wearable item can be dispensed by dispensers, - // equipping it on the target. Default is false. - Dispensable() bool } // ArmourTier represents the tier, or material, that a piece of armour is made of. ArmourTier interface { diff --git a/server/item/component/component.go b/server/item/component/component.go new file mode 100644 index 0000000000..565c948ac4 --- /dev/null +++ b/server/item/component/component.go @@ -0,0 +1,26 @@ +package component + +//go:generate go run ../../../cmd/generate componentgen -output ../../../server/item/component -vanilla-nbt ../../../server/world/vanilla_items.nbt + +// Component is an item component that can be encoded to NBT for the client. +type Component interface { + // ComponentName returns the namespaced component identifier (e.g., "minecraft:wearable"). + ComponentName() string + // Encode returns the component data as a map for NBT serialization. + Encode() (map[string]any, error) +} + +// ComponentItem is implemented by custom items that provide typed components. +// This replaces the monolithic type-assertion approach in iteminternal. +type ComponentItem interface { + ItemComponents() []Component +} + +// RawComponent is an escape hatch for components not yet modeled in the typed API. +type RawComponent struct { + Name string + Data map[string]any +} + +func (r RawComponent) ComponentName() string { return r.Name } +func (r RawComponent) Encode() (map[string]any, error) { return r.Data, nil } diff --git a/server/item/component/component_test.go b/server/item/component/component_test.go new file mode 100644 index 0000000000..961870bf2e --- /dev/null +++ b/server/item/component/component_test.go @@ -0,0 +1,308 @@ +package component + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestComponentEncoders(t *testing.T) { + tests := []struct { + name string + comp Component + wantKeys []string + }{ + { + name: "wearable", + comp: Wearable{ + Slot: SlotArmorHead, + Protection: 3, + HidesPlayerLocation: true, + Dispensable: false, + }, + wantKeys: []string{"slot", "protection", "hides_player_location", "dispensable"}, + }, + { + name: "food", + comp: Food{ + Nutrition: 4, + SaturationModifier: 0.6, + CanAlwaysEat: false, + }, + wantKeys: []string{"nutrition", "saturation_modifier", "can_always_eat"}, + }, + { + name: "durability", + comp: Durability{ + MaxDurability: 100, + DamageChance: [2]int32{1, 100}, + }, + wantKeys: []string{"max_durability", "damage_chance"}, + }, + { + name: "kinetic_weapon", + comp: KineticWeapon{ + Reach: [2]float32{3, 4}, + CreativeReach: [2]float32{5, 6}, + HitboxMargin: 0.5, + DamageMultiplier: 1.5, + DamageModifier: 2.0, + Delay: 10, + DamageConditions: WeaponConditions{ + MaxDuration: 100, + MinSpeed: 0.1, + MinRelativeSpeed: 0, + }, + }, + wantKeys: []string{"creative_reach", "damage_conditions", "damage_modifier", "damage_multiplier", "delay", "hitbox_margin", "knockback_conditions", "reach"}, + }, + { + name: "use_modifiers", + comp: UseModifiers{ + MovementModifier: 0.2, + UseDuration: 1.0, + EmitVibrations: true, + }, + wantKeys: []string{"movement_modifier", "use_duration", "emit_vibrations"}, + }, + { + name: "storage_item", + comp: StorageItem{ + AllowNestedStorageItems: false, + AllowedItems: []string{"minecraft:diamond"}, + BannedItems: []string{"minecraft:bedrock"}, + MaxSlots: 27, + }, + wantKeys: []string{"allow_nested_storage_items", "allowed_items", "banned_items", "max_slots"}, + }, + { + name: "shooter", + comp: Shooter{ + Ammunition: []Ammunition{ + {Item: "minecraft:arrow", SearchInventory: true, UseInCreative: true, UseOffHand: true}, + }, + ChargeOnDraw: true, + MaxDrawDuration: 1.0, + ScalePowerByDrawDuration: true, + }, + wantKeys: []string{"ammunition", "charge_on_draw", "max_draw_duration", "scale_power_by_draw_duration"}, + }, + { + name: "damage", + comp: Damage{Value: 6.0}, + wantKeys: []string{"value"}, + }, + { + name: "cooldown", + comp: Cooldown{ + Category: "test", + Duration: 5.0, + }, + wantKeys: []string{"category", "duration"}, + }, + { + name: "enchantable", + comp: Enchantable{ + Slot: "sword", + Value: 10, + }, + wantKeys: []string{"slot", "value"}, + }, + { + name: "repairable", + comp: Repairable{ + RepairItems: []RepairEntry{ + {Items: []RepairItemEntry{{Name: "minecraft:diamond"}}, RepairAmount: 100}, + }, + }, + wantKeys: []string{"repair_items"}, + }, + { + name: "item_tags", + comp: ItemTags{Tags: []string{"tag1", "tag2"}}, + wantKeys: []string{"tags"}, + }, + { + name: "seed", + comp: Seed{ + CropResult: "minecraft:wheat", + PlantAt: []string{"minecraft:farmland"}, + PlantAtAnySolidSurface: false, + }, + wantKeys: []string{"crop_result", "plant_at", "plant_at_any_solid_surface"}, + }, + { + name: "fuel", + comp: Fuel{Duration: 100}, + wantKeys: []string{"duration"}, + }, + { + name: "fire_resistant", + comp: FireResistant{Value: true}, + wantKeys: []string{"value"}, + }, + { + name: "glint", + comp: Glint{Value: true}, + wantKeys: []string{"value"}, + }, + { + name: "swing_duration", + comp: SwingDuration{Value: 0.5}, + wantKeys: []string{"value"}, + }, + { + name: "swing_sounds", + comp: SwingSounds{ + AttackCriticalHit: "sound1", + AttackHit: "sound2", + AttackMiss: "sound3", + }, + wantKeys: []string{"attack_critical_hit", "attack_hit", "attack_miss"}, + }, + { + name: "piercing_weapon", + comp: PiercingWeapon{ + CreativeReach: [2]float32{5, 6}, + HitboxMargin: 0.5, + Reach: [2]float32{3, 4}, + }, + wantKeys: []string{"creative_reach", "hitbox_margin", "reach"}, + }, + { + name: "camera", + comp: Camera{ + BlackBarsDuration: 0.5, + BlackBarsScreenRatio: 0.3, + PictureDuration: 1.0, + ShutterDuration: 0.1, + ShutterScreenRatio: 0.5, + SlideAwayDuration: 0.5, + }, + wantKeys: []string{"black_bars_duration", "black_bars_screen_ratio", "picture_duration", "shutter_duration", "shutter_screen_ratio", "slide_away_duration"}, + }, + { + name: "block_placer", + comp: BlockPlacer{ + Block: "minecraft:dirt", + ReplaceBlockItem: "minecraft:dirt", + AlignedPlacement: true, + UseOn: []string{"minecraft:grass_block"}, + }, + wantKeys: []string{"block", "replace_block_item", "aligned_placement", "use_on"}, + }, + { + name: "compostable", + comp: Compostable{CompostingChance: 50}, + wantKeys: []string{"composting_chance"}, + }, + { + name: "damage_absorption", + comp: DamageAbsorption{AbsorbableCauses: []string{"fire", "explosion"}}, + wantKeys: []string{"absorbable_causes"}, + }, + { + name: "digger", + comp: Digger{ + DestroySpeeds: []DestroySpeed{{Block: "minecraft:stone", Speed: 5.0}}, + UseEfficiency: true, + }, + wantKeys: []string{"destroy_speeds", "use_efficiency"}, + }, + { + name: "durability_sensor", + comp: DurabilitySensor{ + SoundEvent: "sound1", + DurabilityThresholds: []DurabilityThreshold{ + {Durability: 50, ParticleType: "particle1", SoundEvent: "sound2"}, + }, + }, + wantKeys: []string{"sound_event", "durability_thresholds"}, + }, + { + name: "dyeable", + comp: Dyeable{DefaultColor: [3]int32{255, 0, 0}}, + wantKeys: []string{"default_color"}, + }, + { + name: "entity_placer", + comp: EntityPlacer{ + Entity: "minecraft:pig", + UseOn: []string{"minecraft:grass_block"}, + DispenseOn: []string{"minecraft:dirt"}, + }, + wantKeys: []string{"entity", "use_on", "dispense_on"}, + }, + { + name: "hover_text_color", + comp: HoverTextColor{Value: 0xFF0000}, + wantKeys: []string{"value"}, + }, + { + name: "interact_button", + comp: InteractButton{Value: "test"}, + wantKeys: []string{"value"}, + }, + { + name: "liquid_clipped", + comp: LiquidClipped{Value: true}, + wantKeys: []string{"value"}, + }, + { + name: "rarity", + comp: Rarity{Value: "epic"}, + wantKeys: []string{"value"}, + }, + { + name: "record", + comp: Record{ + ComparatorSignal: 1, + Duration: 10.0, + SoundEvent: "music.record.test", + }, + wantKeys: []string{"comparator_signal", "duration", "sound_event"}, + }, + { + name: "should_despawn", + comp: ShouldDespawn{Value: true}, + wantKeys: []string{"value"}, + }, + { + name: "bundle_interaction", + comp: BundleInteraction{NumViewableSlots: 12}, + wantKeys: []string{"num_viewable_slots"}, + }, + { + name: "storage_weight_limit", + comp: StorageWeightLimit{MaxWeightLimit: 100}, + wantKeys: []string{"max_weight_limit"}, + }, + { + name: "storage_weight_modifier", + comp: StorageWeightModifier{WeightInStorageItem: 5}, + wantKeys: []string{"weight_in_storage_item"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + data, err := tc.comp.Encode() + require.NoError(t, err) + require.Equal(t, "minecraft:"+tc.name, tc.comp.ComponentName()) + for _, k := range tc.wantKeys { + require.Contains(t, data, k, "missing key %s in component %s", k, tc.name) + } + }) + } +} + +func TestRawComponent(t *testing.T) { + raw := RawComponent{ + Name: "minecraft:test", + Data: map[string]any{"key": "value"}, + } + require.Equal(t, "minecraft:test", raw.ComponentName()) + data, err := raw.Encode() + require.NoError(t, err) + require.Equal(t, map[string]any{"key": "value"}, data) +} diff --git a/server/item/component/components_gen.go b/server/item/component/components_gen.go new file mode 100644 index 0000000000..07b856f02c --- /dev/null +++ b/server/item/component/components_gen.go @@ -0,0 +1,852 @@ +// Code generated by componentgen. DO NOT EDIT. +package component + +// Wearable represents the minecraft:wearable component. +type Wearable struct { + Slot SlotArmor + Protection int32 + HidesPlayerLocation bool + Dispensable bool +} + +func (w Wearable) ComponentName() string { return "minecraft:wearable" } + +func (w Wearable) Encode() (map[string]any, error) { + data := map[string]any{} + data["slot"] = w.Slot + data["protection"] = w.Protection + data["hides_player_location"] = w.HidesPlayerLocation + data["dispensable"] = w.Dispensable + return data, nil +} + +// SlotArmor is a named string type for the "minecraft:wearable" slot values. +type SlotArmor string + +const ( + SlotArmorHead SlotArmor = "slot.armor.head" + SlotArmorChest SlotArmor = "slot.armor.chest" + SlotArmorLegs SlotArmor = "slot.armor.legs" + SlotArmorFeet SlotArmor = "slot.armor.feet" +) + +// Food represents the minecraft:food component. +type Food struct { + Nutrition int32 + SaturationModifier float32 + CanAlwaysEat bool + UsingConvertsTo string + OnUseAction int32 + CooldownTime int32 + CooldownType string + Effects []FoodEffect + RemoveEffects []int32 +} + +func (f Food) ComponentName() string { return "minecraft:food" } + +func (f Food) Encode() (map[string]any, error) { + data := map[string]any{} + data["nutrition"] = f.Nutrition + data["saturation_modifier"] = f.SaturationModifier + data["can_always_eat"] = f.CanAlwaysEat + if f.UsingConvertsTo != "" { + data["using_converts_to"] = f.UsingConvertsTo + } + if f.OnUseAction != 0 { + data["on_use_action"] = f.OnUseAction + } + if f.CooldownTime != 0 { + data["cooldown_time"] = f.CooldownTime + } + if f.CooldownType != "" { + data["cooldown_type"] = f.CooldownType + } + if len(f.Effects) > 0 { + data["effects"] = encodeSlice(f.Effects, encodeFoodEffect) + } + if len(f.RemoveEffects) > 0 { + data["remove_effects"] = scalarSlice(f.RemoveEffects) + } + return data, nil +} + +// Durability represents the minecraft:durability component. +type Durability struct { + MaxDurability int32 + DamageChance [2]int32 +} + +func (d Durability) ComponentName() string { return "minecraft:durability" } + +func (d Durability) Encode() (map[string]any, error) { + data := map[string]any{} + data["max_durability"] = d.MaxDurability + data["damage_chance"] = rangeIntData(d.DamageChance) + return data, nil +} + +// KineticWeapon represents the minecraft:kinetic_weapon component. +type KineticWeapon struct { + CreativeReach [2]float32 + DamageConditions WeaponConditions + DamageModifier float32 + DamageMultiplier float32 + Delay int32 + DismountConditions WeaponConditions + HitboxMargin float32 + KnockbackConditions WeaponConditions + Reach [2]float32 +} + +func (k KineticWeapon) ComponentName() string { return "minecraft:kinetic_weapon" } + +func (k KineticWeapon) Encode() (map[string]any, error) { + data := map[string]any{} + data["creative_reach"] = rangeData(k.CreativeReach) + data["damage_conditions"] = encodeWeaponConditions(k.DamageConditions) + data["damage_modifier"] = k.DamageModifier + data["damage_multiplier"] = k.DamageMultiplier + data["delay"] = k.Delay + data["dismount_conditions"] = encodeWeaponConditions(k.DismountConditions) + data["hitbox_margin"] = k.HitboxMargin + data["knockback_conditions"] = encodeWeaponConditions(k.KnockbackConditions) + data["reach"] = rangeData(k.Reach) + return data, nil +} + +// UseModifiers represents the minecraft:use_modifiers component. +type UseModifiers struct { + MovementModifier float32 + UseDuration float32 + EmitVibrations bool + StartSound string + StartUsing string +} + +func (u UseModifiers) ComponentName() string { return "minecraft:use_modifiers" } + +func (u UseModifiers) Encode() (map[string]any, error) { + data := map[string]any{} + data["movement_modifier"] = u.MovementModifier + data["use_duration"] = u.UseDuration + data["emit_vibrations"] = u.EmitVibrations + if u.StartSound != "" { + data["start_sound"] = u.StartSound + } + if u.StartUsing != "" { + data["start_using"] = u.StartUsing + } + return data, nil +} + +// StorageItem represents the minecraft:storage_item component. +type StorageItem struct { + AllowNestedStorageItems bool + AllowedItems []string + BannedItems []string + MaxSlots int32 +} + +func (s StorageItem) ComponentName() string { return "minecraft:storage_item" } + +func (s StorageItem) Encode() (map[string]any, error) { + data := map[string]any{} + data["allow_nested_storage_items"] = s.AllowNestedStorageItems + data["allowed_items"] = stringSlice(s.AllowedItems) + data["banned_items"] = bannedItemsData(s.BannedItems) + data["max_slots"] = s.MaxSlots + return data, nil +} + +// Shooter represents the minecraft:shooter component. +type Shooter struct { + Ammunition []Ammunition + ChargeOnDraw bool + MaxDrawDuration float32 + ScalePowerByDrawDuration bool +} + +func (s Shooter) ComponentName() string { return "minecraft:shooter" } + +func (s Shooter) Encode() (map[string]any, error) { + data := map[string]any{} + data["ammunition"] = encodeSlice(s.Ammunition, encodeAmmunition) + data["charge_on_draw"] = s.ChargeOnDraw + data["max_draw_duration"] = s.MaxDrawDuration + data["scale_power_by_draw_duration"] = s.ScalePowerByDrawDuration + return data, nil +} + +// Projectile represents the minecraft:projectile component. +type Projectile struct { + MinimumCriticalPower float32 + ProjectileEntity string +} + +func (p Projectile) ComponentName() string { return "minecraft:projectile" } + +func (p Projectile) Encode() (map[string]any, error) { + data := map[string]any{} + data["minimum_critical_power"] = p.MinimumCriticalPower + data["projectile_entity"] = p.ProjectileEntity + return data, nil +} + +// Throwable represents the minecraft:throwable component. +type Throwable struct { + DoSwingAnimation bool + LaunchPowerScale float32 + MaxDrawDuration float32 + MaxLaunchPower float32 + MinDrawDuration float32 + ScalePowerByDrawDuration bool +} + +func (t Throwable) ComponentName() string { return "minecraft:throwable" } + +func (t Throwable) Encode() (map[string]any, error) { + data := map[string]any{} + data["do_swing_animation"] = t.DoSwingAnimation + if t.LaunchPowerScale != 0 { + data["launch_power_scale"] = t.LaunchPowerScale + } + if t.MaxDrawDuration != 0 { + data["max_draw_duration"] = t.MaxDrawDuration + } + if t.MaxLaunchPower != 0 { + data["max_launch_power"] = t.MaxLaunchPower + } + if t.MinDrawDuration != 0 { + data["min_draw_duration"] = t.MinDrawDuration + } + if t.ScalePowerByDrawDuration != false { + data["scale_power_by_draw_duration"] = t.ScalePowerByDrawDuration + } + return data, nil +} + +// Damage represents the minecraft:damage component. +type Damage struct { + Value float32 +} + +func (d Damage) ComponentName() string { return "minecraft:damage" } + +func (d Damage) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = d.Value + return data, nil +} + +// Cooldown represents the minecraft:cooldown component. +type Cooldown struct { + Category string + Duration float32 + Type string +} + +func (c Cooldown) ComponentName() string { return "minecraft:cooldown" } + +func (c Cooldown) Encode() (map[string]any, error) { + data := map[string]any{} + data["category"] = c.Category + data["duration"] = c.Duration + if c.Type != "" { + data["type"] = c.Type + } + return data, nil +} + +// Enchantable represents the minecraft:enchantable component. +type Enchantable struct { + Slot string + Value int32 +} + +func (e Enchantable) ComponentName() string { return "minecraft:enchantable" } + +func (e Enchantable) Encode() (map[string]any, error) { + data := map[string]any{} + data["slot"] = e.Slot + data["value"] = e.Value + return data, nil +} + +// Repairable represents the minecraft:repairable component. +type Repairable struct { + RepairItems []RepairEntry +} + +func (r Repairable) ComponentName() string { return "minecraft:repairable" } + +func (r Repairable) Encode() (map[string]any, error) { + data := map[string]any{} + data["repair_items"] = encodeSlice(r.RepairItems, encodeRepairEntry) + return data, nil +} + +// ItemTags represents the minecraft:item_tags component. +type ItemTags struct { + Tags []string +} + +func (i ItemTags) ComponentName() string { return "minecraft:item_tags" } + +func (i ItemTags) Encode() (map[string]any, error) { + data := map[string]any{} + data["tags"] = stringSlice(i.Tags) + return data, nil +} + +// Tags represents the minecraft:tags component. +type Tags struct { + Tags []string +} + +func (t Tags) ComponentName() string { return "minecraft:tags" } + +func (t Tags) Encode() (map[string]any, error) { + data := map[string]any{} + data["tags"] = stringSlice(t.Tags) + return data, nil +} + +// Seed represents the minecraft:seed component. +type Seed struct { + CropResult string + PlantAt []string + PlantAtAnySolidSurface bool + PlantAtFace string +} + +func (s Seed) ComponentName() string { return "minecraft:seed" } + +func (s Seed) Encode() (map[string]any, error) { + data := map[string]any{} + data["crop_result"] = s.CropResult + data["plant_at"] = stringSlice(s.PlantAt) + data["plant_at_any_solid_surface"] = s.PlantAtAnySolidSurface + data["plant_at_face"] = s.PlantAtFace + return data, nil +} + +// Fuel represents the minecraft:fuel component. +type Fuel struct { + Duration float32 +} + +func (f Fuel) ComponentName() string { return "minecraft:fuel" } + +func (f Fuel) Encode() (map[string]any, error) { + data := map[string]any{} + data["duration"] = f.Duration + return data, nil +} + +// FireResistant represents the minecraft:fire_resistant component. +type FireResistant struct { + Value bool +} + +func (f FireResistant) ComponentName() string { return "minecraft:fire_resistant" } + +func (f FireResistant) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = f.Value + return data, nil +} + +// Glint represents the minecraft:glint component. +type Glint struct { + Value bool +} + +func (g Glint) ComponentName() string { return "minecraft:glint" } + +func (g Glint) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = g.Value + return data, nil +} + +// SwingDuration represents the minecraft:swing_duration component. +type SwingDuration struct { + Value float32 +} + +func (s SwingDuration) ComponentName() string { return "minecraft:swing_duration" } + +func (s SwingDuration) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = s.Value + return data, nil +} + +// SwingSounds represents the minecraft:swing_sounds component. +type SwingSounds struct { + AttackCriticalHit string + AttackHit string + AttackMiss string +} + +func (s SwingSounds) ComponentName() string { return "minecraft:swing_sounds" } + +func (s SwingSounds) Encode() (map[string]any, error) { + data := map[string]any{} + data["attack_critical_hit"] = s.AttackCriticalHit + data["attack_hit"] = s.AttackHit + data["attack_miss"] = s.AttackMiss + return data, nil +} + +// PiercingWeapon represents the minecraft:piercing_weapon component. +type PiercingWeapon struct { + CreativeReach [2]float32 + HitboxMargin float32 + Reach [2]float32 +} + +func (p PiercingWeapon) ComponentName() string { return "minecraft:piercing_weapon" } + +func (p PiercingWeapon) Encode() (map[string]any, error) { + data := map[string]any{} + data["creative_reach"] = rangeData(p.CreativeReach) + data["hitbox_margin"] = p.HitboxMargin + data["reach"] = rangeData(p.Reach) + return data, nil +} + +// Camera represents the minecraft:camera component. +type Camera struct { + BlackBarsDuration float32 + BlackBarsScreenRatio float32 + PictureDuration float32 + ShutterDuration float32 + ShutterScreenRatio float32 + SlideAwayDuration float32 + UseDuration int32 +} + +func (c Camera) ComponentName() string { return "minecraft:camera" } + +func (c Camera) Encode() (map[string]any, error) { + data := map[string]any{} + data["black_bars_duration"] = c.BlackBarsDuration + data["black_bars_screen_ratio"] = c.BlackBarsScreenRatio + data["picture_duration"] = c.PictureDuration + data["shutter_duration"] = c.ShutterDuration + data["shutter_screen_ratio"] = c.ShutterScreenRatio + data["slide_away_duration"] = c.SlideAwayDuration + data["use_duration"] = c.UseDuration + return data, nil +} + +// Block represents the minecraft:block component. +type Block struct { + Value string +} + +func (b Block) ComponentName() string { return "minecraft:block" } + +func (b Block) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = b.Value + return data, nil +} + +// BlockPlacer represents the minecraft:block_placer component. +type BlockPlacer struct { + Block string + ReplaceBlockItem string + AlignedPlacement bool + UseOn []string +} + +func (b BlockPlacer) ComponentName() string { return "minecraft:block_placer" } + +func (b BlockPlacer) Encode() (map[string]any, error) { + data := map[string]any{} + data["block"] = b.Block + data["replace_block_item"] = b.ReplaceBlockItem + data["aligned_placement"] = b.AlignedPlacement + if len(b.UseOn) > 0 { + data["use_on"] = stringSlice(b.UseOn) + } + return data, nil +} + +// Compostable represents the minecraft:compostable component. +type Compostable struct { + CompostingChance int32 +} + +func (c Compostable) ComponentName() string { return "minecraft:compostable" } + +func (c Compostable) Encode() (map[string]any, error) { + data := map[string]any{} + data["composting_chance"] = c.CompostingChance + return data, nil +} + +// DamageAbsorption represents the minecraft:damage_absorption component. +type DamageAbsorption struct { + AbsorbableCauses []string +} + +func (d DamageAbsorption) ComponentName() string { return "minecraft:damage_absorption" } + +func (d DamageAbsorption) Encode() (map[string]any, error) { + data := map[string]any{} + data["absorbable_causes"] = stringSlice(d.AbsorbableCauses) + return data, nil +} + +// Digger represents the minecraft:digger component. +type Digger struct { + DestroySpeeds []DestroySpeed + UseEfficiency bool +} + +func (d Digger) ComponentName() string { return "minecraft:digger" } + +func (d Digger) Encode() (map[string]any, error) { + data := map[string]any{} + data["destroy_speeds"] = encodeSlice(d.DestroySpeeds, encodeDestroySpeed) + data["use_efficiency"] = d.UseEfficiency + return data, nil +} + +// DurabilitySensor represents the minecraft:durability_sensor component. +type DurabilitySensor struct { + SoundEvent string + DurabilityThresholds []DurabilityThreshold +} + +func (d DurabilitySensor) ComponentName() string { return "minecraft:durability_sensor" } + +func (d DurabilitySensor) Encode() (map[string]any, error) { + data := map[string]any{} + if d.SoundEvent != "" { + data["sound_event"] = d.SoundEvent + } + if len(d.DurabilityThresholds) > 0 { + data["durability_thresholds"] = encodeSlice(d.DurabilityThresholds, encodeDurabilityThreshold) + } + return data, nil +} + +// Dyeable represents the minecraft:dyeable component. +type Dyeable struct { + DefaultColor [3]int32 +} + +func (d Dyeable) ComponentName() string { return "minecraft:dyeable" } + +func (d Dyeable) Encode() (map[string]any, error) { + data := map[string]any{} + data["default_color"] = list3Data(d.DefaultColor) + return data, nil +} + +// EntityPlacer represents the minecraft:entity_placer component. +type EntityPlacer struct { + Entity string + UseOn []string + DispenseOn []string +} + +func (e EntityPlacer) ComponentName() string { return "minecraft:entity_placer" } + +func (e EntityPlacer) Encode() (map[string]any, error) { + data := map[string]any{} + data["entity"] = e.Entity + if len(e.UseOn) > 0 { + data["use_on"] = stringSlice(e.UseOn) + } + if len(e.DispenseOn) > 0 { + data["dispense_on"] = stringSlice(e.DispenseOn) + } + return data, nil +} + +// HoverTextColor represents the minecraft:hover_text_color component. +type HoverTextColor struct { + Value int32 +} + +func (h HoverTextColor) ComponentName() string { return "minecraft:hover_text_color" } + +func (h HoverTextColor) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = h.Value + return data, nil +} + +// InteractButton represents the minecraft:interact_button component. +type InteractButton struct { + Value string +} + +func (i InteractButton) ComponentName() string { return "minecraft:interact_button" } + +func (i InteractButton) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = i.Value + return data, nil +} + +// LiquidClipped represents the minecraft:liquid_clipped component. +type LiquidClipped struct { + Value bool +} + +func (l LiquidClipped) ComponentName() string { return "minecraft:liquid_clipped" } + +func (l LiquidClipped) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = l.Value + return data, nil +} + +// Rarity represents the minecraft:rarity component. +type Rarity struct { + Value string +} + +func (r Rarity) ComponentName() string { return "minecraft:rarity" } + +func (r Rarity) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = r.Value + return data, nil +} + +// Record represents the minecraft:record component. +type Record struct { + ComparatorSignal int32 + Duration float32 + SoundEvent string +} + +func (r Record) ComponentName() string { return "minecraft:record" } + +func (r Record) Encode() (map[string]any, error) { + data := map[string]any{} + data["comparator_signal"] = r.ComparatorSignal + data["duration"] = r.Duration + data["sound_event"] = r.SoundEvent + return data, nil +} + +// ShouldDespawn represents the minecraft:should_despawn component. +type ShouldDespawn struct { + Value bool +} + +func (s ShouldDespawn) ComponentName() string { return "minecraft:should_despawn" } + +func (s ShouldDespawn) Encode() (map[string]any, error) { + data := map[string]any{} + data["value"] = s.Value + return data, nil +} + +// BundleInteraction represents the minecraft:bundle_interaction component. +type BundleInteraction struct { + NumViewableSlots int32 +} + +func (b BundleInteraction) ComponentName() string { return "minecraft:bundle_interaction" } + +func (b BundleInteraction) Encode() (map[string]any, error) { + data := map[string]any{} + data["num_viewable_slots"] = b.NumViewableSlots + return data, nil +} + +// StorageWeightLimit represents the minecraft:storage_weight_limit component. +type StorageWeightLimit struct { + MaxWeightLimit int32 +} + +func (s StorageWeightLimit) ComponentName() string { return "minecraft:storage_weight_limit" } + +func (s StorageWeightLimit) Encode() (map[string]any, error) { + data := map[string]any{} + data["max_weight_limit"] = s.MaxWeightLimit + return data, nil +} + +// StorageWeightModifier represents the minecraft:storage_weight_modifier component. +type StorageWeightModifier struct { + WeightInStorageItem int32 +} + +func (s StorageWeightModifier) ComponentName() string { return "minecraft:storage_weight_modifier" } + +func (s StorageWeightModifier) Encode() (map[string]any, error) { + data := map[string]any{} + data["weight_in_storage_item"] = s.WeightInStorageItem + return data, nil +} + +// Ammunition represents a nested structure used by a component. +type Ammunition struct { + Item string + SearchInventory bool + UseInCreative bool + UseOffHand bool +} + +// DestroySpeed represents a nested structure used by a component. +type DestroySpeed struct { + Block string + Speed float32 +} + +// DurabilityThreshold represents a nested structure used by a component. +type DurabilityThreshold struct { + Durability int32 + ParticleType string + SoundEvent string +} + +// FoodEffect represents a nested structure used by a component. +type FoodEffect struct { + ID int32 + Duration int32 + Amplifier int32 + Chance float32 + Name string +} + +// RepairEntry represents a nested structure used by a component. +type RepairEntry struct { + Items []RepairItemEntry + RepairAmount int32 +} + +// RepairItemEntry represents a nested structure used by a component. +type RepairItemEntry struct { + Name string + Tags []string +} + +// WeaponConditions represents a nested structure used by a component. +type WeaponConditions struct { + MaxDuration int32 + MinSpeed float32 + MinRelativeSpeed float32 +} + +func encodeAmmunition(v Ammunition) map[string]any { + data := map[string]any{} + data["item"] = v.Item + data["search_inventory"] = v.SearchInventory + data["use_in_creative"] = v.UseInCreative + data["use_offhand"] = v.UseOffHand + return data +} + +func encodeDestroySpeed(v DestroySpeed) map[string]any { + data := map[string]any{} + data["block"] = v.Block + data["speed"] = v.Speed + return data +} + +func encodeDurabilityThreshold(v DurabilityThreshold) map[string]any { + data := map[string]any{} + data["durability"] = v.Durability + if v.ParticleType != "" { + data["particle_type"] = v.ParticleType + } + if v.SoundEvent != "" { + data["sound_event"] = v.SoundEvent + } + return data +} + +func encodeFoodEffect(v FoodEffect) map[string]any { + data := map[string]any{} + data["id"] = v.ID + data["duration"] = v.Duration + data["amplifier"] = v.Amplifier + data["chance"] = v.Chance + if v.Name != "" { + data["name"] = v.Name + } + return data +} + +func encodeRepairEntry(v RepairEntry) map[string]any { + data := map[string]any{} + data["items"] = encodeNameOrTags(v.Items) + data["repair_amount"] = v.RepairAmount + return data +} + +func encodeWeaponConditions(v WeaponConditions) map[string]any { + data := map[string]any{} + data["max_duration"] = v.MaxDuration + data["min_speed"] = v.MinSpeed + data["min_relative_speed"] = v.MinRelativeSpeed + return data +} + +func stringSlice(s []string) []any { + r := make([]any, len(s)) + for i, v := range s { + r[i] = v + } + return r +} + +func scalarSlice[T any](s []T) []any { + r := make([]any, len(s)) + for i, v := range s { + r[i] = v + } + return r +} + +func encodeSlice[V any](vs []V, enc func(V) map[string]any) []any { + r := make([]any, len(vs)) + for i, v := range vs { + r[i] = enc(v) + } + return r +} + +func rangeData(r [2]float32) map[string]any { + return map[string]any{"min": r[0], "max": r[1]} +} + +func rangeIntData(r [2]int32) map[string]any { + return map[string]any{"min": r[0], "max": r[1]} +} + +func list3Data(c [3]int32) []any { + return []any{c[0], c[1], c[2]} +} + +func bannedItemsData(items []string) []any { + banned := make([]any, len(items)) + for i, b := range items { + banned[i] = map[string]any{"name": b} + } + return banned +} + +func encodeNameOrTags(items []RepairItemEntry) []any { + r := make([]any, len(items)) + for i, item := range items { + if item.Name != "" { + r[i] = map[string]any{"name": item.Name} + } else { + r[i] = map[string]any{"tags": stringSlice(item.Tags)} + } + } + return r +} diff --git a/server/item/component/constants.go b/server/item/component/constants.go new file mode 100644 index 0000000000..7d59b1a6df --- /dev/null +++ b/server/item/component/constants.go @@ -0,0 +1,46 @@ +// Code generated by componentgen. DO NOT EDIT. +package component + +// Component name constants +const ( + ComponentWEARABLE = "minecraft:wearable" + ComponentFOOD = "minecraft:food" + ComponentDURABILITY = "minecraft:durability" + ComponentKINETIC_WEAPON = "minecraft:kinetic_weapon" + ComponentUSE_MODIFIERS = "minecraft:use_modifiers" + ComponentSTORAGE_ITEM = "minecraft:storage_item" + ComponentSHOOTER = "minecraft:shooter" + ComponentPROJECTILE = "minecraft:projectile" + ComponentTHROWABLE = "minecraft:throwable" + ComponentDAMAGE = "minecraft:damage" + ComponentCOOLDOWN = "minecraft:cooldown" + ComponentENCHANTABLE = "minecraft:enchantable" + ComponentREPAIRABLE = "minecraft:repairable" + ComponentITEM_TAGS = "minecraft:item_tags" + ComponentTAGS = "minecraft:tags" + ComponentSEED = "minecraft:seed" + ComponentFUEL = "minecraft:fuel" + ComponentFIRE_RESISTANT = "minecraft:fire_resistant" + ComponentGLINT = "minecraft:glint" + ComponentSWING_DURATION = "minecraft:swing_duration" + ComponentSWING_SOUNDS = "minecraft:swing_sounds" + ComponentPIERCING_WEAPON = "minecraft:piercing_weapon" + ComponentCAMERA = "minecraft:camera" + ComponentBLOCK = "minecraft:block" + ComponentBLOCK_PLACER = "minecraft:block_placer" + ComponentCOMPOSTABLE = "minecraft:compostable" + ComponentDAMAGE_ABSORPTION = "minecraft:damage_absorption" + ComponentDIGGER = "minecraft:digger" + ComponentDURABILITY_SENSOR = "minecraft:durability_sensor" + ComponentDYEABLE = "minecraft:dyeable" + ComponentENTITY_PLACER = "minecraft:entity_placer" + ComponentHOVER_TEXT_COLOR = "minecraft:hover_text_color" + ComponentINTERACT_BUTTON = "minecraft:interact_button" + ComponentLIQUID_CLIPPED = "minecraft:liquid_clipped" + ComponentRARITY = "minecraft:rarity" + ComponentRECORD = "minecraft:record" + ComponentSHOULD_DESPAWN = "minecraft:should_despawn" + ComponentBUNDLE_INTERACTION = "minecraft:bundle_interaction" + ComponentSTORAGE_WEIGHT_LIMIT = "minecraft:storage_weight_limit" + ComponentSTORAGE_WEIGHT_MODIFIER = "minecraft:storage_weight_modifier" +) diff --git a/server/item/component/example_test.go b/server/item/component/example_test.go new file mode 100644 index 0000000000..f4269c8d7e --- /dev/null +++ b/server/item/component/example_test.go @@ -0,0 +1,65 @@ +package component_test + +import ( + "fmt" + "image" + + "github.com/df-mc/dragonfly/server/item/category" + "github.com/df-mc/dragonfly/server/item/component" + "github.com/df-mc/dragonfly/server/world" +) + +var _ world.CustomItem = TestHelmet{} +var _ component.ComponentItem = TestHelmet{} + +// TestHelmet is a custom item that demonstrates using the generated typed component +// structs via component.ComponentItem, with the SlotArmor named string type. +type TestHelmet struct{} + +func (TestHelmet) EncodeItem() (name string, meta int16) { + return "test:helmet", 0 +} + +func (TestHelmet) Name() string { + return "Test Helmet" +} + +func (TestHelmet) Texture() image.Image { + return image.NewRGBA(image.Rect(0, 0, 16, 16)) +} + +func (TestHelmet) Category() category.Category { + return category.Equipment() +} + +func (TestHelmet) ItemComponents() []component.Component { + return []component.Component{ + component.Wearable{ + Slot: component.SlotArmorHead, + Protection: 3, + HidesPlayerLocation: false, + Dispensable: true, + }, + component.Durability{ + MaxDurability: 200, + DamageChance: [2]int32{1, 100}, + }, + component.DamageAbsorption{ + AbsorbableCauses: []string{"fall", "explosion"}, + }, + } +} + +func ExampleWearable() { + h := TestHelmet{} + for _, c := range h.ItemComponents() { + if c.ComponentName() != "minecraft:wearable" { + continue + } + data, _ := c.Encode() + fmt.Printf("slot=%v\n", data["slot"]) + break + } + // Output: + // slot=slot.armor.head +} diff --git a/server/item/component/validation.go b/server/item/component/validation.go new file mode 100644 index 0000000000..2182658d7b --- /dev/null +++ b/server/item/component/validation.go @@ -0,0 +1,41 @@ +// Code generated by componentgen. DO NOT EDIT. +package component + +import "fmt" + +// ValidationRules contains cross-component validation rules derived from BDS. +var ValidationRules = map[string][]string{ + "minecraft:kinetic_weapon": {"minecraft:use_modifiers"}, + "minecraft:bundle_interaction": {"minecraft:storage_item"}, + "minecraft:shooter": {"minecraft:use_modifiers"}, +} + +// ValidateComponents checks cross-component constraints. +func ValidateComponents(components map[string]any) error { + // kinetic_weapon requires use_modifiers + if _, ok := components["minecraft:kinetic_weapon"]; ok { + if _, ok := components["minecraft:use_modifiers"]; !ok { + return fmt.Errorf("minecraft:kinetic_weapon requires minecraft:use_modifiers component") + } + } + + // bundle_interaction requires storage_item + if _, ok := components["minecraft:bundle_interaction"]; ok { + if _, ok := components["minecraft:storage_item"]; !ok { + return fmt.Errorf("minecraft:bundle_interaction requires minecraft:storage_item component") + } + } + + // shooter requires use_modifiers with non-zero use_duration + if _, ok := components["minecraft:shooter"]; ok { + if um, ok := components["minecraft:use_modifiers"].(map[string]any); ok { + if ud, ok := um["use_duration"].(float32); !ok || ud == 0 { + return fmt.Errorf("minecraft:shooter requires non-zero use_duration in minecraft:use_modifiers") + } + } else { + return fmt.Errorf("minecraft:shooter requires minecraft:use_modifiers component with non-zero use_duration") + } + } + + return nil +} diff --git a/server/item/tags.go b/server/item/tags.go index e468e7a201..6ff7051b85 100644 --- a/server/item/tags.go +++ b/server/item/tags.go @@ -1,5 +1,106 @@ package item +// Vanilla item tags that may be added to an item using the minecraft:tags item component. Only vanilla item +// tags may use the "minecraft:" namespace. Custom tags may use any other namespace. +const ( + // Armour tags. + TagArmor = "minecraft:is_armor" + TagHorseArmor = "minecraft:horse_armor" + TagNautilusArmor = "minecraft:nautilus_armor" + TagHarness = "minecraft:harness" + + // Food tags. + TagIsMeat = "minecraft:is_meat" + TagIsCooked = "minecraft:is_cooked" + TagIsFood = "minecraft:is_food" + + // Piglin bartering tags. + TagPiglinLoved = "minecraft:piglin_loved" + TagPiglinRepellents = "minecraft:piglin_repellents" + + // Smithing table tags. + TagTransformableItems = "minecraft:transformable_items" + TagTransformMaterials = "minecraft:transform_materials" + TagTransformTemplates = "minecraft:transform_templates" + + // Sulfur cube archetype tags. + TagSulfurCubeArchetypeBouncy = "minecraft:sulfur_cube_archetype_bouncy" + TagSulfurCubeArchetypeRegular = "minecraft:sulfur_cube_archetype_regular" + TagSulfurCubeArchetypeSlowFlat = "minecraft:sulfur_cube_archetype_slow_flat" + TagSulfurCubeArchetypeFastFlat = "minecraft:sulfur_cube_archetype_fast_flat" + TagSulfurCubeArchetypeLight = "minecraft:sulfur_cube_archetype_light" + TagSulfurCubeArchetypeFastSliding = "minecraft:sulfur_cube_archetype_fast_sliding" + TagSulfurCubeArchetypeSlowSliding = "minecraft:sulfur_cube_archetype_slow_sliding" + TagSulfurCubeArchetypeSticky = "minecraft:sulfur_cube_archetype_sticky" + TagSulfurCubeArchetypeHighResistance = "minecraft:sulfur_cube_archetype_high_resistance" + TagSulfurCubeArchetypeExplosive = "minecraft:sulfur_cube_archetype_explosive" + + // Tier tags. + TagChainmailTier = "minecraft:chainmail_tier" + TagCopperTier = "minecraft:copper_tier" + TagDiamondTier = "minecraft:diamond_tier" + TagGoldenTier = "minecraft:golden_tier" + TagIronTier = "minecraft:iron_tier" + TagLeatherTier = "minecraft:leather_tier" + TagNetheriteTier = "minecraft:netherite_tier" + TagStoneTier = "minecraft:stone_tier" + TagWoodenTier = "minecraft:wooden_tier" + + // Tool tags. + TagDigger = "minecraft:digger" + TagIsAxe = "minecraft:is_axe" + TagIsHoe = "minecraft:is_hoe" + TagIsPickaxe = "minecraft:is_pickaxe" + TagIsShears = "minecraft:is_shears" + TagIsShovel = "minecraft:is_shovel" + TagIsSpear = "minecraft:is_spear" + TagIsSword = "minecraft:is_sword" + TagIsTool = "minecraft:is_tool" + TagIsTrident = "minecraft:is_trident" + + // Trim tags. + TagTrimmableArmors = "minecraft:trimmable_armors" + TagTrimMaterials = "minecraft:trim_materials" + TagTrimTemplates = "minecraft:trim_templates" + + // Woodset tags. + TagBoat = "minecraft:boat" + TagBoats = "minecraft:boats" + TagChestBoat = "minecraft:chest_boat" + TagCrimsonStems = "minecraft:crimson_stems" + TagDoor = "minecraft:door" + TagHangingActor = "minecraft:hanging_actor" + TagHangingSign = "minecraft:hanging_sign" + TagLogs = "minecraft:logs" + TagLogsThatBurn = "minecraft:logs_that_burn" + TagMangroveLogs = "minecraft:mangrove_logs" + TagPlanks = "minecraft:planks" + TagSign = "minecraft:sign" + TagWarpedStems = "minecraft:warped_stems" + TagWoodenSlabs = "minecraft:wooden_slabs" + + // Miscellaneous tags. + TagArrow = "minecraft:arrow" + TagBanner = "minecraft:banner" + TagCoals = "minecraft:coals" + TagEgg = "minecraft:egg" + TagIsFish = "minecraft:is_fish" + TagLecternBooks = "minecraft:lectern_books" + TagIsMinecart = "minecraft:is_minecart" + TagMusicDisc = "minecraft:music_disc" + TagSand = "minecraft:sand" + TagSoulFireBaseBlocks = "minecraft:soul_fire_base_blocks" + TagSpawnEgg = "minecraft:spawn_egg" + TagStoneBricks = "minecraft:stone_bricks" + TagStoneCraftingMaterials = "minecraft:stone_crafting_materials" + TagStoneToolMaterials = "minecraft:stone_tool_materials" + TagVibrationDamper = "minecraft:vibration_damper" + TagWool = "minecraft:wool" + TagBookshelfBooks = "minecraft:bookshelf_books" + TagDecoratedPotSherds = "minecraft:decorated_pot_sherds" + TagMetalNuggets = "minecraft:metal_nuggets" +) + // Tagged represents an item that has one or more item tags. These tags may be used by the client for various // purposes, such as determining the tier of an item or checking if an item is food. type Tagged interface { From b2acca4706cc234e2876d6938827f19bfaf31d59 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:30:37 -0600 Subject: [PATCH 09/13] item: remove hand-written item component implementations --- server/internal/iteminternal/components.go | 378 +-------------------- server/item/block_placer.go | 22 -- server/item/camera.go | 26 -- server/item/damage_absorption.go | 8 - server/item/digger.go | 26 -- server/item/durability_sensor.go | 29 -- server/item/dyeable.go | 7 - server/item/enchantable_data.go | 17 - server/item/entity_placer.go | 20 -- server/item/fire_resistant.go | 7 - server/item/food.go | 47 --- server/item/hover_text_color.go | 7 - server/item/icon.go | 9 - server/item/interact_button.go | 8 - server/item/liquid_clipped.go | 7 - server/item/projectile.go | 18 - server/item/rarity.go | 9 - server/item/record.go | 18 - server/item/repair.go | 20 -- server/item/seed.go | 21 -- server/item/shooter.go | 37 -- server/item/should_despawn.go | 7 - server/item/storage.go | 27 -- server/item/swing.go | 25 -- server/item/tags.go | 109 ------ server/item/use_modifiers.go | 23 -- server/item/weapon.go | 59 ---- 27 files changed, 1 insertion(+), 990 deletions(-) delete mode 100644 server/item/block_placer.go delete mode 100644 server/item/camera.go delete mode 100644 server/item/damage_absorption.go delete mode 100644 server/item/digger.go delete mode 100644 server/item/durability_sensor.go delete mode 100644 server/item/dyeable.go delete mode 100644 server/item/enchantable_data.go delete mode 100644 server/item/entity_placer.go delete mode 100644 server/item/fire_resistant.go delete mode 100644 server/item/food.go delete mode 100644 server/item/hover_text_color.go delete mode 100644 server/item/icon.go delete mode 100644 server/item/interact_button.go delete mode 100644 server/item/liquid_clipped.go delete mode 100644 server/item/projectile.go delete mode 100644 server/item/rarity.go delete mode 100644 server/item/record.go delete mode 100644 server/item/repair.go delete mode 100644 server/item/seed.go delete mode 100644 server/item/shooter.go delete mode 100644 server/item/should_despawn.go delete mode 100644 server/item/storage.go delete mode 100644 server/item/swing.go delete mode 100644 server/item/tags.go delete mode 100644 server/item/use_modifiers.go delete mode 100644 server/item/weapon.go diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index 2509cfaf51..aa7baf9727 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -61,49 +61,6 @@ func Components(it world.CustomItem) (map[string]any, error) { }) } if x, ok := it.(item.Consumable); ok { - food := map[string]any{ - "can_always_eat": x.AlwaysConsumable(), - } - if y, ok := it.(item.Food); ok { - info := y.FoodInfo() - food["nutrition"] = int32(info.Nutrition) - food["saturation_modifier"] = float32(info.SaturationModifier) - if info.UsingConvertsTo != "" { - food["using_converts_to"] = info.UsingConvertsTo - } - if info.OnUseAction != 0 { - food["on_use_action"] = int32(info.OnUseAction) - } - if info.CooldownTime != 0 { - food["cooldown_time"] = int32(info.CooldownTime) - food["cooldown_type"] = info.CooldownType - } - if len(info.Effects) != 0 { - effects := make([]any, 0, len(info.Effects)) - for _, e := range info.Effects { - m := map[string]any{ - "id": int32(e.ID), - "duration": int32(e.Duration), - "amplifier": int32(e.Amplifier), - "chance": float32(e.Chance), - } - if e.Name != "" { - m["name"] = e.Name - } - effects = append(effects, m) - } - food["effects"] = effects - } - if len(info.RemoveEffects) != 0 { - removeEffects := make([]any, 0, len(info.RemoveEffects)) - for _, id := range info.RemoveEffects { - removeEffects = append(removeEffects, int32(id)) - } - food["remove_effects"] = removeEffects - } - } - builder.AddComponent("minecraft:food", food) - builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) if y, ok := it.(item.Drinkable); ok && y.Drinkable() { builder.AddProperty("use_animation", int32(2)) @@ -139,14 +96,6 @@ func Components(it world.CustomItem) (map[string]any, error) { if x, ok := it.(item.MaxCounter); ok { builder.AddProperty("max_stack_size", int32(x.MaxCount())) } - if x, ok := it.(item.Icon); ok { - textures := x.IconTextures() - m := make(map[string]any, len(textures)) - for k, v := range textures { - m[k] = v - } - builder.AddProperty("minecraft:icon", map[string]any{"textures": m}) - } if x, ok := it.(item.OffHand); ok { builder.AddProperty("allow_off_hand", x.OffHand()) } @@ -162,13 +111,7 @@ func Components(it world.CustomItem) (map[string]any, error) { if x, ok := it.(item.CanDestroyInCreative); ok { builder.AddProperty("can_destroy_in_creative", x.CanDestroyInCreative()) } - if x, ok := it.(item.Projectile); ok { - info := x.ProjectileInfo() - builder.AddComponent("minecraft:projectile", map[string]any{ - "minimum_critical_power": float32(info.MinimumCriticalPower), - "projectile_entity": info.ProjectileEntity, - }) - } else if _, ok := it.(item.Throwable); ok { + if _, ok := it.(item.Throwable); ok { builder.AddComponent("minecraft:projectile", map[string]any{}) } if x, ok := it.(item.Throwable); ok { @@ -211,228 +154,11 @@ func Components(it world.CustomItem) (map[string]any, error) { "duration": float32(x.FuelInfo().Duration.Seconds()), }) } - if x, ok := it.(item.FireResistant); ok { - builder.AddComponent("minecraft:fire_resistant", map[string]any{ - "value": x.FireResistant(), - }) - } - if x, ok := it.(item.EnchantableData); ok { - info := x.EnchantableData() - builder.AddComponent("minecraft:enchantable", map[string]any{ - "slot": info.Slot, - "value": info.Value, - }) - } - if x, ok := it.(item.RepairMaterials); ok { - builder.AddComponent("minecraft:repairable", map[string]any{ - "repair_items": repairItems(x.RepairMaterials()), - }) - } - if x, ok := it.(item.Tagged); ok { - tags := stringSlice(x.Tags()) - builder.AddComponent("minecraft:item_tags", tags) - builder.AddComponent("minecraft:tags", map[string]any{"tags": tags}) - } - if x, ok := it.(item.Seed); ok { - info := x.SeedInfo() - builder.AddComponent("minecraft:seed", map[string]any{ - "crop_result": info.CropResult, - "plant_at": stringSlice(info.PlantAt), - "plant_at_any_solid_surface": info.PlantAtAnySolidSurface, - "plant_at_face": info.PlantAtFace, - }) - } - if x, ok := it.(item.Storage); ok { - info := x.StorageInfo() - if info.NumViewableSlots != 0 { - if info.NumViewableSlots < 1 || info.NumViewableSlots > 64 { - return nil, fmt.Errorf("NumViewableSlots %d out of range 1-64", info.NumViewableSlots) - } - builder.AddComponent("minecraft:bundle_interaction", map[string]any{ - "num_viewable_slots": int32(info.NumViewableSlots), - }) - } - if info.MaxSlots != 0 || len(info.AllowedItems) != 0 || len(info.BannedItems) != 0 { - builder.AddComponent("minecraft:storage_item", map[string]any{ - "allow_nested_storage_items": info.AllowNestedStorageItems, - "allowed_items": stringSlice(info.AllowedItems), - "banned_items": bannedItems(info.BannedItems), - "max_slots": int32(info.MaxSlots), - }) - } - if info.MaxWeightLimit != 0 { - builder.AddComponent("minecraft:storage_weight_limit", map[string]any{ - "max_weight_limit": int32(info.MaxWeightLimit), - }) - } - if info.WeightInStorageItem != 0 { - builder.AddComponent("minecraft:storage_weight_modifier", map[string]any{ - "weight_in_storage_item": int32(info.WeightInStorageItem), - }) - } - } - if x, ok := it.(item.UseModifiers); ok { - info := x.UseModifiers() - modifiers := map[string]any{ - "emit_vibrations": info.EmitVibrations, - "movement_modifier": float32(info.MovementModifier), - "use_duration": float32(info.UseDuration), - } - if info.StartSound != "" { - modifiers["start_sound"] = info.StartSound - } - if info.StartUsing != "" { - modifiers["start_using"] = info.StartUsing - } - builder.AddComponent("minecraft:use_modifiers", modifiers) - } - if x, ok := it.(item.SwingDuration); ok { - builder.AddComponent("minecraft:swing_duration", map[string]any{ - "value": float32(x.SwingDuration()), - }) - } - if x, ok := it.(item.SwingSounds); ok { - info := x.SwingSounds() - builder.AddComponent("minecraft:swing_sounds", map[string]any{ - "attack_critical_hit": info.AttackCriticalHit, - "attack_hit": info.AttackHit, - "attack_miss": info.AttackMiss, - }) - } - if x, ok := it.(item.KineticWeapon); ok { - builder.AddComponent("minecraft:kinetic_weapon", kineticWeaponData(x.KineticWeaponInfo())) - } - if x, ok := it.(item.PiercingWeapon); ok { - info := x.PiercingWeaponInfo() - builder.AddComponent("minecraft:piercing_weapon", map[string]any{ - "creative_reach": rangeData(info.CreativeReach), - "hitbox_margin": float32(info.HitboxMargin), - "reach": rangeData(info.Reach), - }) - } - if x, ok := it.(item.Camera); ok { - info := x.CameraInfo() - builder.AddComponent("minecraft:camera", map[string]any{ - "black_bars_duration": float32(info.BlackBarsDuration), - "black_bars_screen_ratio": float32(info.BlackBarsScreenRatio), - "picture_duration": float32(info.PictureDuration), - "shutter_duration": float32(info.ShutterDuration), - "shutter_screen_ratio": float32(info.ShutterScreenRatio), - "slide_away_duration": float32(info.SlideAwayDuration), - }) - builder.AddComponent("minecraft:block", "camera") - if info.UseDuration != 0 { - builder.AddProperty("use_duration", int32(info.UseDuration)) - } - } - if x, ok := it.(item.BlockPlacer); ok { - info := x.BlockPlacerInfo() - blockPlacer := map[string]any{ - "block": info.Block, - "replace_block_item": info.ReplaceBlockItem, - "aligned_placement": info.AlignedPlacement, - } - if len(info.UseOn) != 0 { - blockPlacer["use_on"] = stringSlice(info.UseOn) - } - builder.AddComponent("minecraft:block_placer", blockPlacer) - } if x, ok := it.(item.Compostable); ok { builder.AddComponent("minecraft:compostable", map[string]any{ "composting_chance": int32(x.CompostChance() * 100), }) } - if x, ok := it.(item.DamageAbsorption); ok { - builder.AddComponent("minecraft:damage_absorption", map[string]any{ - "absorbable_causes": stringSlice(x.AbsorbableCauses()), - }) - } - if x, ok := it.(item.Digger); ok { - info := x.DiggerInfo() - speeds := make([]any, 0, len(info.DestroySpeeds)) - for _, ds := range info.DestroySpeeds { - speeds = append(speeds, map[string]any{ - "block": ds.Block, - "speed": float32(ds.Speed), - }) - } - builder.AddComponent("minecraft:digger", map[string]any{ - "destroy_speeds": speeds, - "use_efficiency": info.UseEfficiency, - }) - } - if x, ok := it.(item.DurabilitySensor); ok { - info := x.DurabilitySensorInfo() - sensor := map[string]any{} - if info.SoundEvent != "" { - sensor["sound_event"] = info.SoundEvent - } - if len(info.DurabilityThresholds) != 0 { - sensor["durability_thresholds"] = durabilityThresholds(info.DurabilityThresholds) - } - builder.AddComponent("minecraft:durability_sensor", sensor) - } - if x, ok := it.(item.Dyeable); ok { - c := x.DefaultColor() - builder.AddComponent("minecraft:dyeable", map[string]any{ - "default_color": []any{int32(c[0]), int32(c[1]), int32(c[2])}, - }) - } - if x, ok := it.(item.EntityPlacer); ok { - info := x.EntityPlacerInfo() - entityPlacer := map[string]any{ - "entity": info.Entity, - } - if len(info.UseOn) != 0 { - entityPlacer["use_on"] = stringSlice(info.UseOn) - } - if len(info.DispenseOn) != 0 { - entityPlacer["dispense_on"] = stringSlice(info.DispenseOn) - } - builder.AddComponent("minecraft:entity_placer", entityPlacer) - } - if x, ok := it.(item.HoverTextColor); ok { - builder.AddComponent("minecraft:hover_text_color", map[string]any{ - "value": x.HoverTextColor(), - }) - } - if x, ok := it.(item.InteractButton); ok { - builder.AddComponent("minecraft:interact_button", map[string]any{ - "value": x.InteractButton(), - }) - } - if x, ok := it.(item.LiquidClipped); ok { - builder.AddComponent("minecraft:liquid_clipped", map[string]any{ - "value": x.LiquidClipped(), - }) - } - if x, ok := it.(item.Rarity); ok { - builder.AddComponent("minecraft:rarity", map[string]any{ - "value": x.Rarity(), - }) - } - if x, ok := it.(item.Record); ok { - info := x.RecordInfo() - builder.AddComponent("minecraft:record", map[string]any{ - "comparator_signal": int32(info.ComparatorSignal), - "duration": float32(info.Duration), - "sound_event": info.SoundEvent, - }) - } - if x, ok := it.(item.Shooter); ok { - info := x.ShooterInfo() - builder.AddComponent("minecraft:shooter", map[string]any{ - "ammunition": ammunition(info.Ammunition), - "charge_on_draw": info.ChargeOnDraw, - "max_draw_duration": float32(info.MaxDrawDuration), - "scale_power_by_draw_duration": info.ScalePowerByDrawDuration, - }) - } - if x, ok := it.(item.ShouldDespawn); ok { - builder.AddComponent("minecraft:should_despawn", map[string]any{ - "value": x.ShouldDespawn(), - }) - } result := builder.Construct() components := result["components"].(map[string]any) if err := ValidateComponents(components); err != nil { @@ -441,108 +167,6 @@ func Components(it world.CustomItem) (map[string]any, error) { return result, nil } -// ammunition converts a slice of ammunition to the data required for the shooter component. -func ammunition(items []item.Ammunition) []any { - ammo := make([]any, 0, len(items)) - for _, a := range items { - ammo = append(ammo, map[string]any{ - "item": a.Item, - "search_inventory": a.SearchInventory, - "use_in_creative": a.UseInCreative, - "use_offhand": a.UseOffHand, - }) - } - return ammo -} - -// repairItems converts the repair materials of an item to the data required for the repairable -// component. -func repairItems(items []item.RepairItem) []any { - materials := make([]any, 0, len(items)) - for _, r := range items { - var entry []any - if r.Item != "" { - entry = []any{map[string]any{"name": r.Item}} - } else { - entry = []any{map[string]any{"tags": r.Tag}} - } - materials = append(materials, map[string]any{ - "items": entry, - "repair_amount": r.RepairAmount, - }) - } - return materials -} - -// bannedItems converts the banned item identifiers of a storage item to the data required for the -// storage_item component. -func bannedItems(items []string) []any { - banned := make([]any, 0, len(items)) - for _, b := range items { - banned = append(banned, map[string]any{"name": b}) - } - return banned -} - -// rangeData converts a min/max range to the data required for the client. -func rangeData(r [2]float64) map[string]any { - return map[string]any{ - "min": float32(r[0]), - "max": float32(r[1]), - } -} - -// kineticWeaponData converts the kinetic weapon information of an item to the data required for the -// kinetic_weapon component. -func kineticWeaponData(info item.KineticWeaponInfo) map[string]any { - conditions := func(c item.WeaponConditions) map[string]any { - return map[string]any{ - "max_duration": int32(c.MaxDuration), - "min_relative_speed": float32(c.MinRelativeSpeed), - "min_speed": float32(c.MinSpeed), - } - } - return map[string]any{ - "creative_reach": rangeData(info.CreativeReach), - "damage_conditions": conditions(info.DamageConditions), - "damage_modifier": float32(info.DamageModifier), - "damage_multiplier": float32(info.DamageMultiplier), - "delay": int32(info.Delay), - "dismount_conditions": conditions(info.DismountConditions), - "hitbox_margin": float32(info.HitboxMargin), - "knockback_conditions": conditions(info.KnockbackConditions), - "reach": rangeData(info.Reach), - } -} - -// stringSlice converts a slice of strings to a slice of any. -func stringSlice(x []string) []any { - s := make([]any, len(x)) - for i, v := range x { - s[i] = v - } - return s -} - -// durabilityThresholds converts a slice of durability thresholds to the data required for the -// durability_sensor component. -func durabilityThresholds(thresholds []item.DurabilityThreshold) []any { - t := make([]any, 0, len(thresholds)) - for _, th := range thresholds { - m := map[string]any{ - "durability": int32(th.Durability), - } - if th.ParticleType != "" { - m["particle_type"] = th.ParticleType - } - if th.SoundEvent != "" { - m["sound_event"] = th.SoundEvent - } - t = append(t, m) - } - return t -} - // componentsFromComponentItem converts a ComponentItem to a components map. func componentsFromComponentItem(ci component.ComponentItem) (map[string]any, error) { components := make(map[string]any) diff --git a/server/item/block_placer.go b/server/item/block_placer.go deleted file mode 100644 index ff89d4008c..0000000000 --- a/server/item/block_placer.go +++ /dev/null @@ -1,22 +0,0 @@ -package item - -// BlockPlacer represents an item that can place blocks when used. -type BlockPlacer interface { - // BlockPlacerInfo returns the block placer information of the item. - BlockPlacerInfo() BlockPlacerInfo -} - -// BlockPlacerInfo is a struct returned by items that implement BlockPlacer. It contains the block -// placement configuration of the item. -type BlockPlacerInfo struct { - // Block is the identifier of the block that will be placed. - Block string - // UseOn is a list of block identifiers that this item can be used on. If empty, all blocks - // are allowed. - UseOn []string - // ReplaceBlockItem specifies if the item will be registered as the item for this block. - ReplaceBlockItem bool - // AlignedPlacement specifies if block placement through this item is aligned while the - // interaction button is held down. - AlignedPlacement bool -} diff --git a/server/item/camera.go b/server/item/camera.go deleted file mode 100644 index 7258b06bba..0000000000 --- a/server/item/camera.go +++ /dev/null @@ -1,26 +0,0 @@ -package item - -// Camera represents an item that behaves as a camera, allowing the user to take pictures. -type Camera interface { - // CameraInfo returns the camera information of the item. - CameraInfo() CameraInfo -} - -// CameraInfo is a struct returned by items that implement Camera. It contains the information required for -// the client to handle the item as a camera. -type CameraInfo struct { - // BlackBarsDuration is the duration in seconds the black bars are shown when taking a picture. - BlackBarsDuration float64 - // BlackBarsScreenRatio is the ratio of the screen covered by the black bars. - BlackBarsScreenRatio float64 - // PictureDuration is the duration in seconds the picture is shown. - PictureDuration float64 - // ShutterDuration is the duration in seconds the shutter is shown. - ShutterDuration float64 - // ShutterScreenRatio is the ratio of the screen covered by the shutter. - ShutterScreenRatio float64 - // SlideAwayDuration is the duration in seconds the picture slides away. - SlideAwayDuration float64 - // UseDuration is the duration in ticks the item takes to be fully used. - UseDuration int -} diff --git a/server/item/damage_absorption.go b/server/item/damage_absorption.go deleted file mode 100644 index 5ca4c5ab3d..0000000000 --- a/server/item/damage_absorption.go +++ /dev/null @@ -1,8 +0,0 @@ -package item - -// DamageAbsorption represents an item that can absorb damage that would otherwise be dealt to its -// wearer. The item needs a minecraft:durability component for this to function. -type DamageAbsorption interface { - // AbsorbableCauses returns the list of damage causes that can be absorbed by the item. - AbsorbableCauses() []string -} diff --git a/server/item/digger.go b/server/item/digger.go deleted file mode 100644 index f643cf1c41..0000000000 --- a/server/item/digger.go +++ /dev/null @@ -1,26 +0,0 @@ -package item - -// Digger represents an item configured as a digging tool, allowing it to break specific blocks -// faster than normal. -type Digger interface { - // DiggerInfo returns the digger information of the item. - DiggerInfo() DiggerInfo -} - -// DiggerInfo is a struct returned by items that implement Digger. It contains the block-specific -// mining speed configuration of the item. -type DiggerInfo struct { - // DestroySpeeds is a list of block-specific mining speed multipliers. - DestroySpeeds []DestroySpeed - // UseEfficiency specifies if the Efficiency enchantment will increase the dig speed of this - // item. - UseEfficiency bool -} - -// DestroySpeed associates a block with a custom digging speed multiplier. -type DestroySpeed struct { - // Block is the identifier of the block that can be dug. - Block string - // Speed is the digging speed multiplier for the correlating block. - Speed float64 -} diff --git a/server/item/durability_sensor.go b/server/item/durability_sensor.go deleted file mode 100644 index f551c20369..0000000000 --- a/server/item/durability_sensor.go +++ /dev/null @@ -1,29 +0,0 @@ -package item - -// DurabilitySensor represents an item that emits effects when it receives damage. The item also -// needs a minecraft:durability component. -type DurabilitySensor interface { - // DurabilitySensorInfo returns the durability sensor information of the item. - DurabilitySensorInfo() DurabilitySensorInfo -} - -// DurabilitySensorInfo is a struct returned by items that implement DurabilitySensor. It contains -// the thresholds and effects emitted when durability is reduced. -type DurabilitySensorInfo struct { - // SoundEvent is the sound effect emitted when any threshold is met. - SoundEvent string - // DurabilityThresholds is a list of thresholds at which effects are emitted. - DurabilityThresholds []DurabilityThreshold -} - -// DurabilityThreshold defines the durability threshold and effects emitted when that threshold -// is met. -type DurabilityThreshold struct { - // Durability is the durability value at which effects are emitted. Effects are emitted when - // the item durability value is less than or equal to this value. - Durability int - // ParticleType is the particle effect to emit when the threshold is met. - ParticleType string - // SoundEvent is the sound effect to emit when the threshold is met. - SoundEvent string -} diff --git a/server/item/dyeable.go b/server/item/dyeable.go deleted file mode 100644 index f776fae7a9..0000000000 --- a/server/item/dyeable.go +++ /dev/null @@ -1,7 +0,0 @@ -package item - -// Dyeable represents an item that can be dyed using dyes in a crafting grid, like leather armor. -type Dyeable interface { - // DefaultColor returns the default RGB color of the item when undyed. - DefaultColor() [3]uint8 -} diff --git a/server/item/enchantable_data.go b/server/item/enchantable_data.go deleted file mode 100644 index 942a5a40b2..0000000000 --- a/server/item/enchantable_data.go +++ /dev/null @@ -1,17 +0,0 @@ -package item - -// EnchantableData represents an item that can be enchanted and provides the data required for the client to -// display the enchantability of the item. -type EnchantableData interface { - // EnchantableData returns the enchantable data of the item. - EnchantableData() EnchantableInfo -} - -// EnchantableInfo is a struct returned by items that implement EnchantableData. It contains the information -// required for the client to display the enchantability of the item. -type EnchantableInfo struct { - // Slot is the enchantment slot of the item, such as "melee_spear" or "sword". - Slot string - // Value is the enchantment value of the item. - Value uint -} diff --git a/server/item/entity_placer.go b/server/item/entity_placer.go deleted file mode 100644 index 82fbcd647a..0000000000 --- a/server/item/entity_placer.go +++ /dev/null @@ -1,20 +0,0 @@ -package item - -// EntityPlacer represents an item that can place entities into the world, such as spawn eggs. -type EntityPlacer interface { - // EntityPlacerInfo returns the entity placer information of the item. - EntityPlacerInfo() EntityPlacerInfo -} - -// EntityPlacerInfo is a struct returned by items that implement EntityPlacer. It contains the -// entity placement configuration of the item. -type EntityPlacerInfo struct { - // Entity is the identifier of the entity that will be placed. - Entity string - // UseOn is a list of block identifiers that this item can be used on. If empty, all blocks - // are allowed. - UseOn []string - // DispenseOn is a list of block identifiers that this item can be dispensed on. If empty, - // all blocks are allowed. - DispenseOn []string -} diff --git a/server/item/fire_resistant.go b/server/item/fire_resistant.go deleted file mode 100644 index 48cc6c19bf..0000000000 --- a/server/item/fire_resistant.go +++ /dev/null @@ -1,7 +0,0 @@ -package item - -// FireResistant represents an item that is resistant to fire and lava, such as netherite items. -type FireResistant interface { - // FireResistant returns whether the item is resistant to fire and lava. - FireResistant() bool -} diff --git a/server/item/food.go b/server/item/food.go deleted file mode 100644 index 8d3d8711ca..0000000000 --- a/server/item/food.go +++ /dev/null @@ -1,47 +0,0 @@ -package item - -// Food represents an item that has nutritional value and provides the data required for the client to -// display the nutritional value of the item. -type Food interface { - // FoodInfo returns the food information of the item. - FoodInfo() FoodInfo -} - -// FoodInfo is a struct returned by items that implement Food. It contains the information required for the -// client to display the nutritional value of the item. -type FoodInfo struct { - // Nutrition is the number of hunger points the item restores. - Nutrition int - // SaturationModifier is the modifier applied to the saturation restored by the item. - SaturationModifier float64 - // UsingConvertsTo is the identifier of the item the item converts to when consumed, such as a bowl or - // glass bottle. - UsingConvertsTo string - // OnUseAction is the action performed when the item is used, such as the chorus fruit teleport. - OnUseAction int - // OnUseRange is the range in blocks the on use action applies to. - OnUseRange [3]float64 - // CooldownTime is the duration in seconds of the cooldown applied when the item is consumed. - CooldownTime int - // CooldownType is the type of cooldown applied when the item is consumed. - CooldownType string - // Effects is a list of effects applied to the consumer when the item is consumed. - Effects []FoodEffect - // RemoveEffects is a list of effect IDs removed from the consumer when the item is consumed. - RemoveEffects []int -} - -// FoodEffect is a struct returned by items that implement Food. It contains the information required for the -// client to display an effect applied when the item is consumed. -type FoodEffect struct { - // ID is the ID of the effect. - ID int - // Duration is the duration in seconds of the effect. - Duration int - // Amplifier is the amplifier of the effect. - Amplifier int - // Chance is the chance the effect is applied. - Chance float64 - // Name is the name of the effect, such as "hunger" or "poison". - Name string -} diff --git a/server/item/hover_text_color.go b/server/item/hover_text_color.go deleted file mode 100644 index a0d2e1a555..0000000000 --- a/server/item/hover_text_color.go +++ /dev/null @@ -1,7 +0,0 @@ -package item - -// HoverTextColor represents an item with a custom hover text color. -type HoverTextColor interface { - // HoverTextColor returns the color of the item name when hovering over it. - HoverTextColor() string -} diff --git a/server/item/icon.go b/server/item/icon.go deleted file mode 100644 index 93a66b0bd1..0000000000 --- a/server/item/icon.go +++ /dev/null @@ -1,9 +0,0 @@ -package item - -// Icon represents an item with a custom icon texture. The icon textures are the keys from the -// resource_pack/textures/item_texture.json 'texture_data' object associated with the texture file. -type Icon interface { - // IconTextures returns the textures used for the item's icon. The "default" key contains the actual icon - // texture of the item. Additional keys may be used to specify armour trim textures and palettes. - IconTextures() map[string]string -} diff --git a/server/item/interact_button.go b/server/item/interact_button.go deleted file mode 100644 index 4eebcfb726..0000000000 --- a/server/item/interact_button.go +++ /dev/null @@ -1,8 +0,0 @@ -package item - -// InteractButton represents an item that shows an interact button in touch controls. -type InteractButton interface { - // InteractButton returns the text displayed on the interact button. If true, the default - // "Use Item" text will be used. - InteractButton() string -} diff --git a/server/item/liquid_clipped.go b/server/item/liquid_clipped.go deleted file mode 100644 index 41bf96afbb..0000000000 --- a/server/item/liquid_clipped.go +++ /dev/null @@ -1,7 +0,0 @@ -package item - -// LiquidClipped represents an item that interacts with liquid blocks on use. -type LiquidClipped interface { - // LiquidClipped returns whether the item interacts with liquid blocks on use. - LiquidClipped() bool -} diff --git a/server/item/projectile.go b/server/item/projectile.go deleted file mode 100644 index dfb1cd8e6f..0000000000 --- a/server/item/projectile.go +++ /dev/null @@ -1,18 +0,0 @@ -package item - -// Projectile represents an item that is a projectile, which may be shot from dispensers or used as ammunition by -// items implementing Shooter. When combined with Throwable, this specifies the entity spawned when the item is -// thrown. -type Projectile interface { - // ProjectileInfo returns the projectile information of the item. - ProjectileInfo() ProjectileInfo -} - -// ProjectileInfo is a struct returned by items that implement Projectile. It contains the information required for -// the client to use the item as a projectile. -type ProjectileInfo struct { - // ProjectileEntity is the identifier of the entity fired as a projectile. - ProjectileEntity string - // MinimumCriticalPower is how long a player must charge a projectile for it to critically hit, in seconds. - MinimumCriticalPower float64 -} diff --git a/server/item/rarity.go b/server/item/rarity.go deleted file mode 100644 index 57b061f0d0..0000000000 --- a/server/item/rarity.go +++ /dev/null @@ -1,9 +0,0 @@ -package item - -// Rarity represents an item with a specific base rarity that determines the color of the item name -// when hovering over it. -type Rarity interface { - // Rarity returns the base rarity of the item. Valid values are "common", "uncommon", "rare", - // and "epic". - Rarity() string -} diff --git a/server/item/record.go b/server/item/record.go deleted file mode 100644 index 56ba531afc..0000000000 --- a/server/item/record.go +++ /dev/null @@ -1,18 +0,0 @@ -package item - -// Record represents an item that can play music when placed in a jukebox. -type Record interface { - // RecordInfo returns the record information of the item. - RecordInfo() RecordInfo -} - -// RecordInfo is a struct returned by items that implement Record. It contains the music playback -// configuration of the item. -type RecordInfo struct { - // ComparatorSignal is the signal strength for comparator blocks, from 1 to 13. - ComparatorSignal int - // Duration is the duration of the sound event in seconds. - Duration float64 - // SoundEvent is the sound event played by the record. - SoundEvent string -} diff --git a/server/item/repair.go b/server/item/repair.go deleted file mode 100644 index f36d7150db..0000000000 --- a/server/item/repair.go +++ /dev/null @@ -1,20 +0,0 @@ -package item - -// RepairMaterials represents a durable item that can be repaired by other items and provides the data -// required for the client to display the repair materials of the item. -type RepairMaterials interface { - // RepairMaterials returns the repair materials of the item. - RepairMaterials() []RepairItem -} - -// RepairItem is a struct returned by items that implement RepairMaterials. It contains the information -// required for the client to display a single repair material of the item. -type RepairItem struct { - // Item is the identifier of the item used to repair the item. - Item string - // Tag is the tag used to match items that can repair the item. - Tag string - // RepairAmount is the expression used to determine the amount of durability restored when repairing the - // item. - RepairAmount string -} diff --git a/server/item/seed.go b/server/item/seed.go deleted file mode 100644 index 98470f98d1..0000000000 --- a/server/item/seed.go +++ /dev/null @@ -1,21 +0,0 @@ -package item - -// Seed represents an item that can be planted on blocks to grow a crop. -type Seed interface { - // SeedInfo returns the information of the item related to planting it as a seed. - SeedInfo() SeedInfo -} - -// SeedInfo is a struct returned by items that implement Seed. It contains the information required for the -// client to allow planting the item on the specified blocks. -type SeedInfo struct { - // CropResult is the identifier of the crop that grows when the seed is planted. - CropResult string - // PlantAt is a list of block identifiers that the seed may be planted on. This list is ignored if - // PlantAtAnySolidSurface is true. - PlantAt []string - // PlantAtAnySolidSurface is true if the seed may be planted on any solid surface. - PlantAtAnySolidSurface bool - // PlantAtFace is the face the seed is planted on, such as "up" or "down". - PlantAtFace string -} diff --git a/server/item/shooter.go b/server/item/shooter.go deleted file mode 100644 index 181b5ae46d..0000000000 --- a/server/item/shooter.go +++ /dev/null @@ -1,37 +0,0 @@ -package item - -// Shooter represents an item that is able to shoot projectiles, similarly to a bow or crossbow. Ammunition used by -// the item must implement Projectile in order to function properly. -type Shooter interface { - // ShooterInfo returns the shooter information of the item. - ShooterInfo() ShooterInfo -} - -// ShooterInfo is a struct returned by items that implement Shooter. It contains the information required for the -// client to shoot projectiles using the item. -type ShooterInfo struct { - // Ammunition is a list of ammunition entries that define which items can be used as projectiles for this - // shooter. - Ammunition []Ammunition - // MaxDrawDuration is the maximum time in seconds that a player can draw the shooter before it automatically - // fires or reaches maximum power. - MaxDrawDuration float64 - // ChargeOnDraw is true if the shooter begins charging when the player starts drawing, similar to a crossbow. - ChargeOnDraw bool - // ScalePowerByDrawDuration is true if the projectile's launch power increases based on how long the player - // holds the use button before releasing. - ScalePowerByDrawDuration bool -} - -// Ammunition represents an entry of ammunition that can be used by a Shooter. It specifies the item to be used as -// a projectile and where it may be taken from. -type Ammunition struct { - // Item is the identifier of the item used as ammunition. - Item string - // SearchInventory is true if the inventory of the user may be searched for the ammunition. - SearchInventory bool - // UseInCreative is true if the ammunition may be used in creative mode. - UseInCreative bool - // UseOffHand is true if the off-hand of the user may be used for the ammunition. - UseOffHand bool -} diff --git a/server/item/should_despawn.go b/server/item/should_despawn.go deleted file mode 100644 index 09fcdb6f8f..0000000000 --- a/server/item/should_despawn.go +++ /dev/null @@ -1,7 +0,0 @@ -package item - -// ShouldDespawn represents an item that has a configurable despawn behaviour while floating in the world. -type ShouldDespawn interface { - // ShouldDespawn returns whether the item should eventually despawn while floating in the world. - ShouldDespawn() bool -} diff --git a/server/item/storage.go b/server/item/storage.go deleted file mode 100644 index 7f57ac720d..0000000000 --- a/server/item/storage.go +++ /dev/null @@ -1,27 +0,0 @@ -package item - -// Storage represents an item that can store other items, such as a bundle or shulker box. -type Storage interface { - // StorageInfo returns the information of the item related to storing other items inside it. - StorageInfo() StorageInfo -} - -// StorageInfo is a struct returned by items that implement Storage. It contains the information required for -// the client to allow storing other items inside the item. -type StorageInfo struct { - // MaxSlots is the maximum number of slots the item may store items in. - MaxSlots int - // MaxWeightLimit is the maximum weight of items the item may store. - MaxWeightLimit int - // WeightInStorageItem is the weight of the item itself when stored inside another storage item. - WeightInStorageItem int - // NumViewableSlots is the number of slots that may be viewed when interacting with the item. If set to - // zero, no bundle interaction component is sent. Default is 12. Value must be >= 1. Value must be <= 64. - NumViewableSlots uint8 - // AllowNestedStorageItems is true if other storage items may be stored inside the item. - AllowNestedStorageItems bool - // AllowedItems is a list of item identifiers that may be stored inside the item. - AllowedItems []string - // BannedItems is a list of item identifiers that may not be stored inside the item. - BannedItems []string -} diff --git a/server/item/swing.go b/server/item/swing.go deleted file mode 100644 index 01ae5eb9ae..0000000000 --- a/server/item/swing.go +++ /dev/null @@ -1,25 +0,0 @@ -package item - -// SwingDuration represents an item with a custom duration in seconds of the swing animation played when the -// item is used to attack. -type SwingDuration interface { - // SwingDuration returns the duration in seconds of the swing animation. - SwingDuration() float64 -} - -// SwingSounds represents an item with custom sounds played when the item is swung. -type SwingSounds interface { - // SwingSounds returns the sounds played when the item is swung. - SwingSounds() SwingSoundsInfo -} - -// SwingSoundsInfo is a struct returned by items that implement SwingSounds. It contains the sounds played -// when the item is swung. -type SwingSoundsInfo struct { - // AttackCriticalHit is the sound played when an attack made with the item hits and deals critical damage. - AttackCriticalHit string - // AttackHit is the sound played when an attack made with the item hits. - AttackHit string - // AttackMiss is the sound played when an attack made with the item misses. - AttackMiss string -} diff --git a/server/item/tags.go b/server/item/tags.go deleted file mode 100644 index 6ff7051b85..0000000000 --- a/server/item/tags.go +++ /dev/null @@ -1,109 +0,0 @@ -package item - -// Vanilla item tags that may be added to an item using the minecraft:tags item component. Only vanilla item -// tags may use the "minecraft:" namespace. Custom tags may use any other namespace. -const ( - // Armour tags. - TagArmor = "minecraft:is_armor" - TagHorseArmor = "minecraft:horse_armor" - TagNautilusArmor = "minecraft:nautilus_armor" - TagHarness = "minecraft:harness" - - // Food tags. - TagIsMeat = "minecraft:is_meat" - TagIsCooked = "minecraft:is_cooked" - TagIsFood = "minecraft:is_food" - - // Piglin bartering tags. - TagPiglinLoved = "minecraft:piglin_loved" - TagPiglinRepellents = "minecraft:piglin_repellents" - - // Smithing table tags. - TagTransformableItems = "minecraft:transformable_items" - TagTransformMaterials = "minecraft:transform_materials" - TagTransformTemplates = "minecraft:transform_templates" - - // Sulfur cube archetype tags. - TagSulfurCubeArchetypeBouncy = "minecraft:sulfur_cube_archetype_bouncy" - TagSulfurCubeArchetypeRegular = "minecraft:sulfur_cube_archetype_regular" - TagSulfurCubeArchetypeSlowFlat = "minecraft:sulfur_cube_archetype_slow_flat" - TagSulfurCubeArchetypeFastFlat = "minecraft:sulfur_cube_archetype_fast_flat" - TagSulfurCubeArchetypeLight = "minecraft:sulfur_cube_archetype_light" - TagSulfurCubeArchetypeFastSliding = "minecraft:sulfur_cube_archetype_fast_sliding" - TagSulfurCubeArchetypeSlowSliding = "minecraft:sulfur_cube_archetype_slow_sliding" - TagSulfurCubeArchetypeSticky = "minecraft:sulfur_cube_archetype_sticky" - TagSulfurCubeArchetypeHighResistance = "minecraft:sulfur_cube_archetype_high_resistance" - TagSulfurCubeArchetypeExplosive = "minecraft:sulfur_cube_archetype_explosive" - - // Tier tags. - TagChainmailTier = "minecraft:chainmail_tier" - TagCopperTier = "minecraft:copper_tier" - TagDiamondTier = "minecraft:diamond_tier" - TagGoldenTier = "minecraft:golden_tier" - TagIronTier = "minecraft:iron_tier" - TagLeatherTier = "minecraft:leather_tier" - TagNetheriteTier = "minecraft:netherite_tier" - TagStoneTier = "minecraft:stone_tier" - TagWoodenTier = "minecraft:wooden_tier" - - // Tool tags. - TagDigger = "minecraft:digger" - TagIsAxe = "minecraft:is_axe" - TagIsHoe = "minecraft:is_hoe" - TagIsPickaxe = "minecraft:is_pickaxe" - TagIsShears = "minecraft:is_shears" - TagIsShovel = "minecraft:is_shovel" - TagIsSpear = "minecraft:is_spear" - TagIsSword = "minecraft:is_sword" - TagIsTool = "minecraft:is_tool" - TagIsTrident = "minecraft:is_trident" - - // Trim tags. - TagTrimmableArmors = "minecraft:trimmable_armors" - TagTrimMaterials = "minecraft:trim_materials" - TagTrimTemplates = "minecraft:trim_templates" - - // Woodset tags. - TagBoat = "minecraft:boat" - TagBoats = "minecraft:boats" - TagChestBoat = "minecraft:chest_boat" - TagCrimsonStems = "minecraft:crimson_stems" - TagDoor = "minecraft:door" - TagHangingActor = "minecraft:hanging_actor" - TagHangingSign = "minecraft:hanging_sign" - TagLogs = "minecraft:logs" - TagLogsThatBurn = "minecraft:logs_that_burn" - TagMangroveLogs = "minecraft:mangrove_logs" - TagPlanks = "minecraft:planks" - TagSign = "minecraft:sign" - TagWarpedStems = "minecraft:warped_stems" - TagWoodenSlabs = "minecraft:wooden_slabs" - - // Miscellaneous tags. - TagArrow = "minecraft:arrow" - TagBanner = "minecraft:banner" - TagCoals = "minecraft:coals" - TagEgg = "minecraft:egg" - TagIsFish = "minecraft:is_fish" - TagLecternBooks = "minecraft:lectern_books" - TagIsMinecart = "minecraft:is_minecart" - TagMusicDisc = "minecraft:music_disc" - TagSand = "minecraft:sand" - TagSoulFireBaseBlocks = "minecraft:soul_fire_base_blocks" - TagSpawnEgg = "minecraft:spawn_egg" - TagStoneBricks = "minecraft:stone_bricks" - TagStoneCraftingMaterials = "minecraft:stone_crafting_materials" - TagStoneToolMaterials = "minecraft:stone_tool_materials" - TagVibrationDamper = "minecraft:vibration_damper" - TagWool = "minecraft:wool" - TagBookshelfBooks = "minecraft:bookshelf_books" - TagDecoratedPotSherds = "minecraft:decorated_pot_sherds" - TagMetalNuggets = "minecraft:metal_nuggets" -) - -// Tagged represents an item that has one or more item tags. These tags may be used by the client for various -// purposes, such as determining the tier of an item or checking if an item is food. -type Tagged interface { - // Tags returns the tags of the item. - Tags() []string -} diff --git a/server/item/use_modifiers.go b/server/item/use_modifiers.go deleted file mode 100644 index a35ce7e27f..0000000000 --- a/server/item/use_modifiers.go +++ /dev/null @@ -1,23 +0,0 @@ -package item - -// UseModifiers represents an item that modifies the way it is used, such as slowing down the user while it is -// being used. -type UseModifiers interface { - // UseModifiers returns the use modifiers of the item. - UseModifiers() UseModifiersInfo -} - -// UseModifiersInfo is a struct returned by items that implement UseModifiers. It contains the information -// required for the client to modify the way the item is used. -type UseModifiersInfo struct { - // MovementModifier is the modifier applied to the movement speed of the user while using the item. - MovementModifier float64 - // UseDuration is the duration in seconds the item takes to be fully used. - UseDuration float64 - // EmitVibrations is true if using the item emits vibrations. - EmitVibrations bool - // StartSound is the sound played when the item starts being used. - StartSound string - // StartUsing is the condition required to start using the item, such as "always" or "require_charging". - StartUsing string -} diff --git a/server/item/weapon.go b/server/item/weapon.go deleted file mode 100644 index 58e5b37834..0000000000 --- a/server/item/weapon.go +++ /dev/null @@ -1,59 +0,0 @@ -package item - -// KineticWeapon represents a weapon that deals damage based on the kinetic energy of the user's movement, -// such as a spear. -type KineticWeapon interface { - // KineticWeaponInfo returns the kinetic weapon information of the item. - KineticWeaponInfo() KineticWeaponInfo -} - -// KineticWeaponInfo is a struct returned by items that implement KineticWeapon. It contains the information -// required for the client to handle the item as a kinetic weapon. -type KineticWeaponInfo struct { - // Reach is the minimum and maximum reach of the weapon. - Reach [2]float64 - // CreativeReach is the minimum and maximum reach of the weapon in creative mode. - CreativeReach [2]float64 - // HitboxMargin is the margin of the hitbox of the weapon. - HitboxMargin float64 - // DamageMultiplier is the multiplier applied to the damage dealt by the weapon. - DamageMultiplier float64 - // DamageModifier is the modifier applied to the damage dealt by the weapon. - DamageModifier float64 - // Delay is the delay in ticks before the weapon can be used again. - Delay int - // DamageConditions are the conditions required for the weapon to deal its full damage. - DamageConditions WeaponConditions - // KnockbackConditions are the conditions required for the weapon to knock back its target. - KnockbackConditions WeaponConditions - // DismountConditions are the conditions required for the weapon to dismount its target. - DismountConditions WeaponConditions -} - -// WeaponConditions is a struct returned by items that implement KineticWeapon. It contains the conditions -// required for the weapon to apply certain effects. -type WeaponConditions struct { - // MaxDuration is the maximum duration in ticks for which the condition applies. - MaxDuration int - // MinSpeed is the minimum speed of the user for which the condition applies. - MinSpeed float64 - // MinRelativeSpeed is the minimum relative speed of the user for which the condition applies. - MinRelativeSpeed float64 -} - -// PiercingWeapon represents a weapon that can pierce through targets, such as a spear. -type PiercingWeapon interface { - // PiercingWeaponInfo returns the piercing weapon information of the item. - PiercingWeaponInfo() PiercingWeaponInfo -} - -// PiercingWeaponInfo is a struct returned by items that implement PiercingWeapon. It contains the information -// required for the client to handle the item as a piercing weapon. -type PiercingWeaponInfo struct { - // Reach is the minimum and maximum reach of the weapon. - Reach [2]float64 - // CreativeReach is the minimum and maximum reach of the weapon in creative mode. - CreativeReach [2]float64 - // HitboxMargin is the margin of the hitbox of the weapon. - HitboxMargin float64 -} From 76fa30e74fa4fd709a1b1155ab5d5f82e07ff87b Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:42:15 -0600 Subject: [PATCH 10/13] server: Reverted breaking component changes. --- server/internal/iteminternal/components.go | 42 ++---------------- server/item/durability.go | 3 -- server/item/item.go | 51 +--------------------- 3 files changed, 5 insertions(+), 91 deletions(-) diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index aa7baf9727..e315a40335 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -73,9 +73,6 @@ func Components(it world.CustomItem) (map[string]any, error) { "category": name, "duration": float32(x.Cooldown().Seconds()), } - if y, ok := it.(item.CooldownTyped); ok { - cooldown["type"] = y.CooldownType() - } builder.AddComponent("minecraft:cooldown", cooldown) } if x, ok := it.(item.Durable); ok { @@ -84,10 +81,6 @@ func Components(it world.CustomItem) (map[string]any, error) { "min": int32(100), "max": int32(100), } - if info.DamageChance != [2]int{} { - damageChance["min"] = int32(info.DamageChance[0]) - damageChance["max"] = int32(info.DamageChance[1]) - } builder.AddComponent("minecraft:durability", map[string]any{ "max_durability": int32(info.MaxDurability), "damage_chance": damageChance, @@ -99,42 +92,13 @@ func Components(it world.CustomItem) (map[string]any, error) { if x, ok := it.(item.OffHand); ok { builder.AddProperty("allow_off_hand", x.OffHand()) } - if x, ok := it.(item.StackedByData); ok { - builder.AddProperty("stacked_by_data", x.StackedByData()) - } - if x, ok := it.(item.MiningSpeed); ok { - builder.AddProperty("mining_speed", float32(x.MiningSpeed())) - } - if x, ok := it.(item.FrameCount); ok { - builder.AddProperty("frame_count", int32(x.FrameCount())) - } - if x, ok := it.(item.CanDestroyInCreative); ok { - builder.AddProperty("can_destroy_in_creative", x.CanDestroyInCreative()) - } if _, ok := it.(item.Throwable); ok { builder.AddComponent("minecraft:projectile", map[string]any{}) } if x, ok := it.(item.Throwable); ok { - info := x.ThrowableInfo() - throwable := map[string]any{ - "do_swing_animation": info.SwingAnimation, - } - if info.LaunchPowerScale != 0 { - throwable["launch_power_scale"] = float32(info.LaunchPowerScale) - } - if info.MaxDrawDuration != 0 { - throwable["max_draw_duration"] = float32(info.MaxDrawDuration) - } - if info.MaxLaunchPower != 0 { - throwable["max_launch_power"] = float32(info.MaxLaunchPower) - } - if info.MinDrawDuration != 0 { - throwable["min_draw_duration"] = float32(info.MinDrawDuration) - } - if info.ScalePowerByDrawDuration { - throwable["scale_power_by_draw_duration"] = true - } - builder.AddComponent("minecraft:throwable", throwable) + builder.AddComponent("minecraft:throwable", map[string]any{ + "do_swing_animation": x.SwingAnimation(), + }) } if x, ok := it.(item.Glinted); ok { builder.AddComponent("minecraft:glint", map[string]any{ diff --git a/server/item/durability.go b/server/item/durability.go index 213328de5c..eff07c0f60 100644 --- a/server/item/durability.go +++ b/server/item/durability.go @@ -19,9 +19,6 @@ type DurabilityInfo struct { // AttackDurability and BreakDurability are the losses in durability that the item sustains when they are // used to do the respective actions. AttackDurability, BreakDurability int - // DamageChance is the percentage chance of the item losing durability when damaged, as a min and max value. - // Both values default to 100 if unset. - DamageChance [2]int // Persistent is true if the item is persistent, i.e. it will not be destroyed when at its last durability stage. Persistent bool } diff --git a/server/item/item.go b/server/item/item.go index 205021fa83..33a503cd2a 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -51,25 +51,8 @@ type Usable interface { // Throwable represents a custom item that can be thrown such as a projectile. This will only have an effect on // non-vanilla items. type Throwable interface { - // ThrowableInfo returns the throwable information of the item. - ThrowableInfo() ThrowableInfo -} - -// ThrowableInfo is a struct returned by items that implement Throwable. It contains the information required for -// the client to throw the item. -type ThrowableInfo struct { - // SwingAnimation is true if the client should cause the player's arm to swing when the item is thrown. - SwingAnimation bool - // LaunchPowerScale is the scale at which the power of the throw increases. - LaunchPowerScale float64 - // MaxDrawDuration is the maximum duration to draw a throwable item, in seconds. - MaxDrawDuration float64 - // MaxLaunchPower is the maximum power to launch the throwable item. - MaxLaunchPower float64 - // MinDrawDuration is the minimum duration to draw a throwable item, in seconds. - MinDrawDuration float64 - // ScalePowerByDrawDuration is true if the power of the throw increases with the duration charged. - ScalePowerByDrawDuration bool + // SwingAnimation returns true if the client should cause the player's arm to swing when the item is thrown. + SwingAnimation() bool } // OffHand represents an item that can be held in the off hand. @@ -154,36 +137,6 @@ type Cooldown interface { Cooldown() time.Duration } -// CooldownTyped represents an item that has a typed cooldown, such as an attack or use cooldown. -type CooldownTyped interface { - // CooldownType returns the type of cooldown of the item, such as "attack" or "use". - CooldownType() string -} - -// StackedByData represents an item that is stacked by its metadata value, such as fish or golden apples. -type StackedByData interface { - // StackedByData returns whether the item is stacked by its metadata value. - StackedByData() bool -} - -// MiningSpeed represents an item with a custom mining speed. -type MiningSpeed interface { - // MiningSpeed returns the mining speed of the item. - MiningSpeed() float64 -} - -// FrameCount represents an item with a custom amount of animation frames in its icon texture. -type FrameCount interface { - // FrameCount returns the amount of animation frames in the icon texture of the item. - FrameCount() int -} - -// CanDestroyInCreative represents an item that can be used to destroy blocks in creative mode. -type CanDestroyInCreative interface { - // CanDestroyInCreative returns whether the item can be used to destroy blocks in creative mode. - CanDestroyInCreative() bool -} - // nameable represents a block that may be named. These are often containers such as chests, which have a // name displayed in their interface. type nameable interface { From 6f91330c4856844a7772932defea9112e3b164ac Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:27:54 -0600 Subject: [PATCH 11/13] components.go: Add back random things that were removed. --- server/internal/iteminternal/components.go | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/internal/iteminternal/components.go b/server/internal/iteminternal/components.go index e315a40335..733e80aa4d 100644 --- a/server/internal/iteminternal/components.go +++ b/server/internal/iteminternal/components.go @@ -62,6 +62,10 @@ func Components(it world.CustomItem) (map[string]any, error) { } if x, ok := it.(item.Consumable); ok { builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20)) + builder.AddComponent("minecraft:food", map[string]any{ + "can_always_eat": x.AlwaysConsumable(), + }) + if y, ok := it.(item.Drinkable); ok && y.Drinkable() { builder.AddProperty("use_animation", int32(2)) } else { @@ -69,21 +73,18 @@ func Components(it world.CustomItem) (map[string]any, error) { } } if x, ok := it.(item.Cooldown); ok { - cooldown := map[string]any{ + builder.AddComponent("minecraft:cooldown", map[string]any{ "category": name, "duration": float32(x.Cooldown().Seconds()), - } - builder.AddComponent("minecraft:cooldown", cooldown) + }) } if x, ok := it.(item.Durable); ok { - info := x.DurabilityInfo() - damageChance := map[string]any{ - "min": int32(100), - "max": int32(100), - } builder.AddComponent("minecraft:durability", map[string]any{ - "max_durability": int32(info.MaxDurability), - "damage_chance": damageChance, + "max_durability": int32(x.DurabilityInfo().MaxDurability), + "damage_chance": map[string]any{ + "min": int32(100), + "max": int32(100), + }, }) } if x, ok := it.(item.MaxCounter); ok { @@ -93,6 +94,8 @@ func Components(it world.CustomItem) (map[string]any, error) { builder.AddProperty("allow_off_hand", x.OffHand()) } if _, ok := it.(item.Throwable); ok { + // The data in minecraft:projectile is only used by vanilla server-side, but we must send at least an empty map + // so the client will play the throwing animation. builder.AddComponent("minecraft:projectile", map[string]any{}) } if x, ok := it.(item.Throwable); ok { From f8296afdb5feb03dade6567e3e406dd09a240022 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:51:53 -0600 Subject: [PATCH 12/13] Change constants casing to PascalCase. --- .../componentgen/generate/generator.go | 20 ++++- server/item/component/constants.go | 80 +++++++++---------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/cmd/generate/componentgen/generate/generator.go b/cmd/generate/componentgen/generate/generator.go index 835c3e3592..707f4ebcd3 100644 --- a/cmd/generate/componentgen/generate/generator.go +++ b/cmd/generate/componentgen/generate/generator.go @@ -4,9 +4,11 @@ import ( "bytes" "fmt" "path/filepath" + "slices" "sort" "strings" "text/template" + "unicode" "github.com/df-mc/dragonfly/cmd/generate/componentgen/parse" ) @@ -130,10 +132,20 @@ func componentNameToStruct(name string) string { } func componentNameToConst(name string) string { - // Remove "minecraft:" prefix - name = strings.TrimPrefix(name, "minecraft:") - // Convert to CONSTANT_CASE - return strings.ToUpper(strings.ReplaceAll(name, ":", "_")) + // Remove "minecraft:" prefix and split parts. + parts := strings.Split(strings.TrimPrefix(name, "minecraft:"), "_") + + parts = slices.DeleteFunc(parts, func(w string) bool { return w == "" }) // Remove empty parts + + for i, word := range parts { + if len(word) == 0 { + continue + } + // Capitalize first letter, lowercase the rest + parts[i] = string(unicode.ToUpper(rune(word[0]))) + strings.ToLower(word[1:]) + } + + return strings.Join(parts, "") } const constantsTemplate = `// Code generated by componentgen. DO NOT EDIT. diff --git a/server/item/component/constants.go b/server/item/component/constants.go index 7d59b1a6df..f34966f5a8 100644 --- a/server/item/component/constants.go +++ b/server/item/component/constants.go @@ -3,44 +3,44 @@ package component // Component name constants const ( - ComponentWEARABLE = "minecraft:wearable" - ComponentFOOD = "minecraft:food" - ComponentDURABILITY = "minecraft:durability" - ComponentKINETIC_WEAPON = "minecraft:kinetic_weapon" - ComponentUSE_MODIFIERS = "minecraft:use_modifiers" - ComponentSTORAGE_ITEM = "minecraft:storage_item" - ComponentSHOOTER = "minecraft:shooter" - ComponentPROJECTILE = "minecraft:projectile" - ComponentTHROWABLE = "minecraft:throwable" - ComponentDAMAGE = "minecraft:damage" - ComponentCOOLDOWN = "minecraft:cooldown" - ComponentENCHANTABLE = "minecraft:enchantable" - ComponentREPAIRABLE = "minecraft:repairable" - ComponentITEM_TAGS = "minecraft:item_tags" - ComponentTAGS = "minecraft:tags" - ComponentSEED = "minecraft:seed" - ComponentFUEL = "minecraft:fuel" - ComponentFIRE_RESISTANT = "minecraft:fire_resistant" - ComponentGLINT = "minecraft:glint" - ComponentSWING_DURATION = "minecraft:swing_duration" - ComponentSWING_SOUNDS = "minecraft:swing_sounds" - ComponentPIERCING_WEAPON = "minecraft:piercing_weapon" - ComponentCAMERA = "minecraft:camera" - ComponentBLOCK = "minecraft:block" - ComponentBLOCK_PLACER = "minecraft:block_placer" - ComponentCOMPOSTABLE = "minecraft:compostable" - ComponentDAMAGE_ABSORPTION = "minecraft:damage_absorption" - ComponentDIGGER = "minecraft:digger" - ComponentDURABILITY_SENSOR = "minecraft:durability_sensor" - ComponentDYEABLE = "minecraft:dyeable" - ComponentENTITY_PLACER = "minecraft:entity_placer" - ComponentHOVER_TEXT_COLOR = "minecraft:hover_text_color" - ComponentINTERACT_BUTTON = "minecraft:interact_button" - ComponentLIQUID_CLIPPED = "minecraft:liquid_clipped" - ComponentRARITY = "minecraft:rarity" - ComponentRECORD = "minecraft:record" - ComponentSHOULD_DESPAWN = "minecraft:should_despawn" - ComponentBUNDLE_INTERACTION = "minecraft:bundle_interaction" - ComponentSTORAGE_WEIGHT_LIMIT = "minecraft:storage_weight_limit" - ComponentSTORAGE_WEIGHT_MODIFIER = "minecraft:storage_weight_modifier" + ComponentWearable = "minecraft:wearable" + ComponentFood = "minecraft:food" + ComponentDurability = "minecraft:durability" + ComponentKineticWeapon = "minecraft:kinetic_weapon" + ComponentUseModifiers = "minecraft:use_modifiers" + ComponentStorageItem = "minecraft:storage_item" + ComponentShooter = "minecraft:shooter" + ComponentProjectile = "minecraft:projectile" + ComponentThrowable = "minecraft:throwable" + ComponentDamage = "minecraft:damage" + ComponentCooldown = "minecraft:cooldown" + ComponentEnchantable = "minecraft:enchantable" + ComponentRepairable = "minecraft:repairable" + ComponentItemTags = "minecraft:item_tags" + ComponentTags = "minecraft:tags" + ComponentSeed = "minecraft:seed" + ComponentFuel = "minecraft:fuel" + ComponentFireResistant = "minecraft:fire_resistant" + ComponentGlint = "minecraft:glint" + ComponentSwingDuration = "minecraft:swing_duration" + ComponentSwingSounds = "minecraft:swing_sounds" + ComponentPiercingWeapon = "minecraft:piercing_weapon" + ComponentCamera = "minecraft:camera" + ComponentBlock = "minecraft:block" + ComponentBlockPlacer = "minecraft:block_placer" + ComponentCompostable = "minecraft:compostable" + ComponentDamageAbsorption = "minecraft:damage_absorption" + ComponentDigger = "minecraft:digger" + ComponentDurabilitySensor = "minecraft:durability_sensor" + ComponentDyeable = "minecraft:dyeable" + ComponentEntityPlacer = "minecraft:entity_placer" + ComponentHoverTextColor = "minecraft:hover_text_color" + ComponentInteractButton = "minecraft:interact_button" + ComponentLiquidClipped = "minecraft:liquid_clipped" + ComponentRarity = "minecraft:rarity" + ComponentRecord = "minecraft:record" + ComponentShouldDespawn = "minecraft:should_despawn" + ComponentBundleInteraction = "minecraft:bundle_interaction" + ComponentStorageWeightLimit = "minecraft:storage_weight_limit" + ComponentStorageWeightModifier = "minecraft:storage_weight_modifier" ) From 0f1a19cc30e75ac0799ed3cb4d50c9ba3c5cde89 Mon Sep 17 00:00:00 2001 From: TrippleAWap <90356816+TrippleAWap@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:58:37 -0600 Subject: [PATCH 13/13] remove unwated tests --- .../internal/iteminternal/validation_test.go | 164 ---------- server/item/component/component_test.go | 308 ------------------ 2 files changed, 472 deletions(-) delete mode 100644 server/internal/iteminternal/validation_test.go delete mode 100644 server/item/component/component_test.go diff --git a/server/internal/iteminternal/validation_test.go b/server/internal/iteminternal/validation_test.go deleted file mode 100644 index 942fc9dae5..0000000000 --- a/server/internal/iteminternal/validation_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package iteminternal - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestValidation(t *testing.T) { - tests := []struct { - name string - components map[string]any - wantErr bool - errMsg string - }{ - { - name: "kinetic_weapon without use_modifiers", - components: map[string]any{ - "minecraft:kinetic_weapon": map[string]any{ - "damage_conditions": map[string]any{"max_duration": int32(100)}, - }, - }, - wantErr: true, - errMsg: "minecraft:kinetic_weapon requires minecraft:use_modifiers component", - }, - { - name: "kinetic_weapon with use_modifiers", - components: map[string]any{ - "minecraft:kinetic_weapon": map[string]any{ - "damage_conditions": map[string]any{"max_duration": int32(100)}, - }, - "minecraft:use_modifiers": map[string]any{ - "use_duration": float32(1.0), - "movement_modifier": float32(0.2), - }, - }, - wantErr: false, - }, - { - name: "kinetic_weapon with invalid conditions (both min_speed and min_relative_speed)", - components: map[string]any{ - "minecraft:kinetic_weapon": map[string]any{ - "damage_conditions": map[string]any{ - "max_duration": int32(100), - "min_speed": float32(0.1), - "min_relative_speed": float32(0.2), - }, - }, - "minecraft:use_modifiers": map[string]any{ - "use_duration": float32(1.0), - }, - }, - wantErr: true, - errMsg: "min_speed and min_relative_speed are mutually exclusive", - }, - { - name: "kinetic_weapon with zero max_duration", - components: map[string]any{ - "minecraft:kinetic_weapon": map[string]any{ - "damage_conditions": map[string]any{ - "max_duration": int32(0), - }, - }, - "minecraft:use_modifiers": map[string]any{ - "use_duration": float32(1.0), - }, - }, - wantErr: true, - errMsg: "at least one condition with max_duration > 0 required", - }, - { - name: "bundle_interaction without storage_item", - components: map[string]any{ - "minecraft:bundle_interaction": map[string]any{ - "num_viewable_slots": int32(12), - }, - }, - wantErr: true, - errMsg: "minecraft:bundle_interaction requires minecraft:storage_item component", - }, - { - name: "bundle_interaction with storage_item", - components: map[string]any{ - "minecraft:bundle_interaction": map[string]any{ - "num_viewable_slots": int32(12), - }, - "minecraft:storage_item": map[string]any{ - "max_slots": int32(27), - }, - }, - wantErr: false, - }, - { - name: "shooter without use_modifiers", - components: map[string]any{ - "minecraft:shooter": map[string]any{ - "ammunition": []any{}, - }, - }, - wantErr: true, - errMsg: "minecraft:shooter requires minecraft:use_modifiers component with non-zero use_duration", - }, - { - name: "shooter with use_modifiers but zero use_duration", - components: map[string]any{ - "minecraft:shooter": map[string]any{ - "ammunition": []any{}, - }, - "minecraft:use_modifiers": map[string]any{ - "use_duration": float32(0), - }, - }, - wantErr: true, - errMsg: "minecraft:shooter requires non-zero use_duration in minecraft:use_modifiers", - }, - { - name: "shooter with valid use_modifiers", - components: map[string]any{ - "minecraft:shooter": map[string]any{ - "ammunition": []any{}, - }, - "minecraft:use_modifiers": map[string]any{ - "use_duration": float32(1.0), - }, - }, - wantErr: false, - }, - { - name: "repairable with invalid repair_items", - components: map[string]any{ - "minecraft:repairable": map[string]any{ - "repair_items": []any{ - map[string]any{"invalid": "entry"}, - }, - }, - }, - wantErr: true, - errMsg: "minecraft:repairable repair_items must have 'items' field", - }, - { - name: "repairable with valid repair_items", - components: map[string]any{ - "minecraft:repairable": map[string]any{ - "repair_items": []any{ - map[string]any{"items": []any{map[string]any{"name": "minecraft:diamond"}}, "repair_amount": int32(100)}, - }, - }, - }, - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := ValidateComponents(tc.components) - if tc.wantErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/server/item/component/component_test.go b/server/item/component/component_test.go deleted file mode 100644 index 961870bf2e..0000000000 --- a/server/item/component/component_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package component - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestComponentEncoders(t *testing.T) { - tests := []struct { - name string - comp Component - wantKeys []string - }{ - { - name: "wearable", - comp: Wearable{ - Slot: SlotArmorHead, - Protection: 3, - HidesPlayerLocation: true, - Dispensable: false, - }, - wantKeys: []string{"slot", "protection", "hides_player_location", "dispensable"}, - }, - { - name: "food", - comp: Food{ - Nutrition: 4, - SaturationModifier: 0.6, - CanAlwaysEat: false, - }, - wantKeys: []string{"nutrition", "saturation_modifier", "can_always_eat"}, - }, - { - name: "durability", - comp: Durability{ - MaxDurability: 100, - DamageChance: [2]int32{1, 100}, - }, - wantKeys: []string{"max_durability", "damage_chance"}, - }, - { - name: "kinetic_weapon", - comp: KineticWeapon{ - Reach: [2]float32{3, 4}, - CreativeReach: [2]float32{5, 6}, - HitboxMargin: 0.5, - DamageMultiplier: 1.5, - DamageModifier: 2.0, - Delay: 10, - DamageConditions: WeaponConditions{ - MaxDuration: 100, - MinSpeed: 0.1, - MinRelativeSpeed: 0, - }, - }, - wantKeys: []string{"creative_reach", "damage_conditions", "damage_modifier", "damage_multiplier", "delay", "hitbox_margin", "knockback_conditions", "reach"}, - }, - { - name: "use_modifiers", - comp: UseModifiers{ - MovementModifier: 0.2, - UseDuration: 1.0, - EmitVibrations: true, - }, - wantKeys: []string{"movement_modifier", "use_duration", "emit_vibrations"}, - }, - { - name: "storage_item", - comp: StorageItem{ - AllowNestedStorageItems: false, - AllowedItems: []string{"minecraft:diamond"}, - BannedItems: []string{"minecraft:bedrock"}, - MaxSlots: 27, - }, - wantKeys: []string{"allow_nested_storage_items", "allowed_items", "banned_items", "max_slots"}, - }, - { - name: "shooter", - comp: Shooter{ - Ammunition: []Ammunition{ - {Item: "minecraft:arrow", SearchInventory: true, UseInCreative: true, UseOffHand: true}, - }, - ChargeOnDraw: true, - MaxDrawDuration: 1.0, - ScalePowerByDrawDuration: true, - }, - wantKeys: []string{"ammunition", "charge_on_draw", "max_draw_duration", "scale_power_by_draw_duration"}, - }, - { - name: "damage", - comp: Damage{Value: 6.0}, - wantKeys: []string{"value"}, - }, - { - name: "cooldown", - comp: Cooldown{ - Category: "test", - Duration: 5.0, - }, - wantKeys: []string{"category", "duration"}, - }, - { - name: "enchantable", - comp: Enchantable{ - Slot: "sword", - Value: 10, - }, - wantKeys: []string{"slot", "value"}, - }, - { - name: "repairable", - comp: Repairable{ - RepairItems: []RepairEntry{ - {Items: []RepairItemEntry{{Name: "minecraft:diamond"}}, RepairAmount: 100}, - }, - }, - wantKeys: []string{"repair_items"}, - }, - { - name: "item_tags", - comp: ItemTags{Tags: []string{"tag1", "tag2"}}, - wantKeys: []string{"tags"}, - }, - { - name: "seed", - comp: Seed{ - CropResult: "minecraft:wheat", - PlantAt: []string{"minecraft:farmland"}, - PlantAtAnySolidSurface: false, - }, - wantKeys: []string{"crop_result", "plant_at", "plant_at_any_solid_surface"}, - }, - { - name: "fuel", - comp: Fuel{Duration: 100}, - wantKeys: []string{"duration"}, - }, - { - name: "fire_resistant", - comp: FireResistant{Value: true}, - wantKeys: []string{"value"}, - }, - { - name: "glint", - comp: Glint{Value: true}, - wantKeys: []string{"value"}, - }, - { - name: "swing_duration", - comp: SwingDuration{Value: 0.5}, - wantKeys: []string{"value"}, - }, - { - name: "swing_sounds", - comp: SwingSounds{ - AttackCriticalHit: "sound1", - AttackHit: "sound2", - AttackMiss: "sound3", - }, - wantKeys: []string{"attack_critical_hit", "attack_hit", "attack_miss"}, - }, - { - name: "piercing_weapon", - comp: PiercingWeapon{ - CreativeReach: [2]float32{5, 6}, - HitboxMargin: 0.5, - Reach: [2]float32{3, 4}, - }, - wantKeys: []string{"creative_reach", "hitbox_margin", "reach"}, - }, - { - name: "camera", - comp: Camera{ - BlackBarsDuration: 0.5, - BlackBarsScreenRatio: 0.3, - PictureDuration: 1.0, - ShutterDuration: 0.1, - ShutterScreenRatio: 0.5, - SlideAwayDuration: 0.5, - }, - wantKeys: []string{"black_bars_duration", "black_bars_screen_ratio", "picture_duration", "shutter_duration", "shutter_screen_ratio", "slide_away_duration"}, - }, - { - name: "block_placer", - comp: BlockPlacer{ - Block: "minecraft:dirt", - ReplaceBlockItem: "minecraft:dirt", - AlignedPlacement: true, - UseOn: []string{"minecraft:grass_block"}, - }, - wantKeys: []string{"block", "replace_block_item", "aligned_placement", "use_on"}, - }, - { - name: "compostable", - comp: Compostable{CompostingChance: 50}, - wantKeys: []string{"composting_chance"}, - }, - { - name: "damage_absorption", - comp: DamageAbsorption{AbsorbableCauses: []string{"fire", "explosion"}}, - wantKeys: []string{"absorbable_causes"}, - }, - { - name: "digger", - comp: Digger{ - DestroySpeeds: []DestroySpeed{{Block: "minecraft:stone", Speed: 5.0}}, - UseEfficiency: true, - }, - wantKeys: []string{"destroy_speeds", "use_efficiency"}, - }, - { - name: "durability_sensor", - comp: DurabilitySensor{ - SoundEvent: "sound1", - DurabilityThresholds: []DurabilityThreshold{ - {Durability: 50, ParticleType: "particle1", SoundEvent: "sound2"}, - }, - }, - wantKeys: []string{"sound_event", "durability_thresholds"}, - }, - { - name: "dyeable", - comp: Dyeable{DefaultColor: [3]int32{255, 0, 0}}, - wantKeys: []string{"default_color"}, - }, - { - name: "entity_placer", - comp: EntityPlacer{ - Entity: "minecraft:pig", - UseOn: []string{"minecraft:grass_block"}, - DispenseOn: []string{"minecraft:dirt"}, - }, - wantKeys: []string{"entity", "use_on", "dispense_on"}, - }, - { - name: "hover_text_color", - comp: HoverTextColor{Value: 0xFF0000}, - wantKeys: []string{"value"}, - }, - { - name: "interact_button", - comp: InteractButton{Value: "test"}, - wantKeys: []string{"value"}, - }, - { - name: "liquid_clipped", - comp: LiquidClipped{Value: true}, - wantKeys: []string{"value"}, - }, - { - name: "rarity", - comp: Rarity{Value: "epic"}, - wantKeys: []string{"value"}, - }, - { - name: "record", - comp: Record{ - ComparatorSignal: 1, - Duration: 10.0, - SoundEvent: "music.record.test", - }, - wantKeys: []string{"comparator_signal", "duration", "sound_event"}, - }, - { - name: "should_despawn", - comp: ShouldDespawn{Value: true}, - wantKeys: []string{"value"}, - }, - { - name: "bundle_interaction", - comp: BundleInteraction{NumViewableSlots: 12}, - wantKeys: []string{"num_viewable_slots"}, - }, - { - name: "storage_weight_limit", - comp: StorageWeightLimit{MaxWeightLimit: 100}, - wantKeys: []string{"max_weight_limit"}, - }, - { - name: "storage_weight_modifier", - comp: StorageWeightModifier{WeightInStorageItem: 5}, - wantKeys: []string{"weight_in_storage_item"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - data, err := tc.comp.Encode() - require.NoError(t, err) - require.Equal(t, "minecraft:"+tc.name, tc.comp.ComponentName()) - for _, k := range tc.wantKeys { - require.Contains(t, data, k, "missing key %s in component %s", k, tc.name) - } - }) - } -} - -func TestRawComponent(t *testing.T) { - raw := RawComponent{ - Name: "minecraft:test", - Data: map[string]any{"key": "value"}, - } - require.Equal(t, "minecraft:test", raw.ComponentName()) - data, err := raw.Encode() - require.NoError(t, err) - require.Equal(t, map[string]any{"key": "value"}, data) -}