diff --git a/assets/entities.json b/assets/entities.json index d0422db71..26e612bdf 100644 --- a/assets/entities.json +++ b/assets/entities.json @@ -17,6 +17,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:acacia_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "acacia_chest_boat": { @@ -37,6 +52,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:acacia_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "allay": { @@ -197,6 +227,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:bamboo_chest_raft" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "bamboo_raft": { @@ -217,6 +262,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:bamboo_raft" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "bat": { @@ -287,6 +347,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:birch_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "birch_chest_boat": { @@ -307,6 +382,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:birch_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "blaze": { @@ -840,6 +930,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:cherry_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "cherry_chest_boat": { @@ -860,6 +965,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:cherry_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "chest_minecart": { @@ -1407,6 +1527,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:dark_oak_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "dark_oak_chest_boat": { @@ -1427,6 +1562,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:dark_oak_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "dolphin": { @@ -3389,6 +3539,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:jungle_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "jungle_chest_boat": { @@ -3409,6 +3574,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:jungle_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "leash_knot": { @@ -3700,6 +3880,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:mangrove_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "mangrove_chest_boat": { @@ -3720,6 +3915,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:mangrove_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "mannequin": { @@ -4037,6 +4247,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:oak_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "oak_chest_boat": { @@ -4057,6 +4282,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:oak_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "ocelot": { @@ -4142,6 +4382,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:pale_oak_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "pale_oak_chest_boat": { @@ -4162,6 +4417,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:pale_oak_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "panda": { @@ -6206,6 +6476,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:spruce_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "spruce_chest_boat": { @@ -6226,6 +6511,21 @@ "spawn_restriction": { "location": "UNRESTRICTED", "heightmap": "MOTION_BLOCKING_NO_LEAVES" + }, + "loot_table": { + "type": "minecraft:entity", + "pools": [ + { + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:spruce_chest_boat" + } + ], + "rolls": 1.0, + "bonus_rolls": 0.0 + } + ] } }, "squid": { @@ -8013,4 +8313,4 @@ "heightmap": "MOTION_BLOCKING_NO_LEAVES" } } -} \ No newline at end of file +} diff --git a/pumpkin-data/src/generated/entity_type.rs b/pumpkin-data/src/generated/entity_type.rs index 6cd60c56e..80ba2596a 100644 --- a/pumpkin-data/src/generated/entity_type.rs +++ b/pumpkin-data/src/generated/entity_type.rs @@ -153,7 +153,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:acacia_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -173,7 +189,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:acacia_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -329,7 +361,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:bamboo_chest_raft", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -349,7 +397,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:bamboo_raft", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -417,7 +481,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:birch_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -437,7 +517,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:birch_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -913,7 +1009,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:cherry_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -933,7 +1045,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:cherry_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -1346,7 +1474,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:dark_oak_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -1366,7 +1510,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:dark_oak_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -2976,7 +3136,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:jungle_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -2996,7 +3172,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:jungle_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3242,7 +3434,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:mangrove_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3262,7 +3470,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:mangrove_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3521,7 +3745,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:oak_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3541,7 +3781,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:oak_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3625,7 +3881,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:pale_oak_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -3645,7 +3917,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:pale_oak_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -5110,7 +5398,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:spruce_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { @@ -5130,7 +5434,23 @@ impl EntityType { fire_immune: false, category: &MobCategory::MISC, can_spawn_far_from_player: true, - loot_table: None, + loot_table: Some(LootTable { + r#type: LootTableType::Entity, + random_sequence: None, + pools: Some(&[LootPool { + entries: &[LootPoolEntry { + content: LootPoolEntryTypes::Item(ItemEntry { + name: "minecraft:spruce_chest_boat", + }), + conditions: None, + functions: None, + }], + rolls: LootNumberProviderTypes::Constant(1f32), + bonus_rolls: 0f32, + conditions: None, + functions: None, + }]), + }), dimension: [1.375f32, 0.5625f32], eye_height: 0.5625f32, spawn_restriction: SpawnRestriction { diff --git a/pumpkin-data/src/generated/fluid.rs b/pumpkin-data/src/generated/fluid.rs index 6c9cfd5bb..698552277 100644 --- a/pumpkin-data/src/generated/fluid.rs +++ b/pumpkin-data/src/generated/fluid.rs @@ -639,31 +639,14 @@ impl Fluid { #[allow(unreachable_patterns, clippy::match_overlapping_arm)] pub const fn from_state_id(id: u16) -> Option<&'static Self> { match id { - 0u16..=0u16 => Some(&Fluid::EMPTY), - 86u16..=86u16 => Some(&Fluid::WATER), - 86u16..=101u16 => Some(&Fluid::FLOWING_WATER), - 102u16..=102u16 => Some(&Fluid::LAVA), - 102u16..=117u16 => Some(&Fluid::FLOWING_LAVA), + 0..=0 => Some(&Fluid::EMPTY), + 86..=86 => Some(&Fluid::WATER), + 102..=102 => Some(&Fluid::LAVA), + 86..=101 => Some(&Fluid::FLOWING_WATER), + 102..=117 => Some(&Fluid::FLOWING_LAVA), _ => None, } } - pub fn same_fluid_type(a: u16, b: u16) -> bool { - a == b - || (a == 1 && b == 2) - || (a == 2 && b == 1) - || (a == 3 && b == 4) - || (a == 4 && b == 3) - } - pub fn matches_type(&self, other: &Fluid) -> bool { - Self::same_fluid_type(self.id, other.id) - } - pub fn to_flowing(&self) -> &'static Fluid { - match self.id { - 2 => &Fluid::FLOWING_WATER, - 4 => &Fluid::FLOWING_LAVA, - _ => Fluid::from_id(self.id).unwrap_or(&Fluid::EMPTY), - } - } pub fn ident_to_fluid_id(name: &str) -> Option { match name { "empty" => Some(0), @@ -714,6 +697,23 @@ impl Fluid { _ => panic!("Invalid props"), } } + pub fn same_fluid_type(a: u16, b: u16) -> bool { + a == b + || (a == 1 && b == 2) + || (a == 2 && b == 1) + || (a == 3 && b == 4) + || (a == 4 && b == 3) + } + pub fn matches_type(&self, other: &Fluid) -> bool { + Self::same_fluid_type(self.id, other.id) + } + pub fn to_flowing(&self) -> &'static Fluid { + match self.id { + 2 => &Fluid::FLOWING_WATER, + 4 => &Fluid::FLOWING_LAVA, + _ => Fluid::from_id(self.id).unwrap_or(&Fluid::EMPTY), + } + } pub fn is_source(&self, state_id: u16) -> bool { let idx = (state_id as usize) % self.states.len(); self.states[idx].is_source diff --git a/pumpkin-data/src/generated/tag.rs b/pumpkin-data/src/generated/tag.rs index 3aef632c8..b976373c1 100644 --- a/pumpkin-data/src/generated/tag.rs +++ b/pumpkin-data/src/generated/tag.rs @@ -3,57 +3,57 @@ use pumpkin_util::version::MinecraftVersion; pub type Tag = (&'static [&'static str], &'static [u16]); #[derive(Eq, PartialEq, Hash, Debug, Clone, Copy)] pub enum RegistryKey { - BannerPattern, - DamageType, - Item, - Instrument, - WorldgenBiome, - Fluid, PaintingVariant, - Timeline, - GameEvent, + WorldgenBiome, + Instrument, Block, - PointOfInterestType, - EntityType, + BannerPattern, + Item, + GameEvent, + Timeline, Dialog, Enchantment, + Fluid, + PointOfInterestType, + DamageType, + EntityType, } impl RegistryKey { pub fn from_string(s: &str) -> Option { match s { - "banner_pattern" => Some(Self::BannerPattern), - "damage_type" => Some(Self::DamageType), - "item" => Some(Self::Item), - "instrument" => Some(Self::Instrument), - "worldgen/biome" => Some(Self::WorldgenBiome), - "fluid" => Some(Self::Fluid), "painting_variant" => Some(Self::PaintingVariant), - "timeline" => Some(Self::Timeline), - "game_event" => Some(Self::GameEvent), + "worldgen/biome" => Some(Self::WorldgenBiome), + "instrument" => Some(Self::Instrument), "block" => Some(Self::Block), - "point_of_interest_type" => Some(Self::PointOfInterestType), - "entity_type" => Some(Self::EntityType), + "banner_pattern" => Some(Self::BannerPattern), + "item" => Some(Self::Item), + "game_event" => Some(Self::GameEvent), + "timeline" => Some(Self::Timeline), "dialog" => Some(Self::Dialog), "enchantment" => Some(Self::Enchantment), + "fluid" => Some(Self::Fluid), + "point_of_interest_type" => Some(Self::PointOfInterestType), + "damage_type" => Some(Self::DamageType), + "entity_type" => Some(Self::EntityType), _ => None, } } pub fn identifier_string(&self) -> &str { match self { - Self::BannerPattern => "banner_pattern", - Self::DamageType => "damage_type", - Self::Item => "item", - Self::Instrument => "instrument", - Self::WorldgenBiome => "worldgen/biome", - Self::Fluid => "fluid", Self::PaintingVariant => "painting_variant", - Self::Timeline => "timeline", - Self::GameEvent => "game_event", + Self::WorldgenBiome => "worldgen/biome", + Self::Instrument => "instrument", Self::Block => "block", - Self::PointOfInterestType => "point_of_interest_type", - Self::EntityType => "entity_type", + Self::BannerPattern => "banner_pattern", + Self::Item => "item", + Self::GameEvent => "game_event", + Self::Timeline => "timeline", Self::Dialog => "dialog", Self::Enchantment => "enchantment", + Self::Fluid => "fluid", + Self::PointOfInterestType => "point_of_interest_type", + Self::DamageType => "damage_type", + Self::EntityType => "entity_type", } } } diff --git a/pumpkin-protocol/src/java/client/play/mod.rs b/pumpkin-protocol/src/java/client/play/mod.rs index e3b033df1..ceccfefc4 100644 --- a/pumpkin-protocol/src/java/client/play/mod.rs +++ b/pumpkin-protocol/src/java/client/play/mod.rs @@ -67,6 +67,7 @@ mod set_equipment; mod set_experience; mod set_health; mod set_held_item; +mod set_passengers; mod set_player_inventory; mod set_time; mod set_title; @@ -160,6 +161,7 @@ pub use set_equipment::*; pub use set_experience::*; pub use set_health::*; pub use set_held_item::*; +pub use set_passengers::*; pub use set_player_inventory::*; pub use set_time::*; pub use set_title::*; diff --git a/pumpkin-protocol/src/java/client/play/set_passengers.rs b/pumpkin-protocol/src/java/client/play/set_passengers.rs new file mode 100644 index 000000000..365ff4743 --- /dev/null +++ b/pumpkin-protocol/src/java/client/play/set_passengers.rs @@ -0,0 +1,22 @@ +use pumpkin_data::packet::clientbound::PLAY_SET_PASSENGERS; +use pumpkin_macros::java_packet; +use serde::Serialize; + +use crate::VarInt; + +#[derive(Serialize)] +#[java_packet(PLAY_SET_PASSENGERS)] +pub struct CSetPassengers<'a> { + pub entity_id: VarInt, + pub passengers: &'a [VarInt], +} + +impl<'a> CSetPassengers<'a> { + #[must_use] + pub const fn new(entity_id: VarInt, passengers: &'a [VarInt]) -> Self { + Self { + entity_id, + passengers, + } + } +} diff --git a/pumpkin-protocol/src/java/server/play/mod.rs b/pumpkin-protocol/src/java/server/play/mod.rs index 8c34a5fd7..9faae6a1c 100644 --- a/pumpkin-protocol/src/java/server/play/mod.rs +++ b/pumpkin-protocol/src/java/server/play/mod.rs @@ -13,6 +13,8 @@ mod cookie_response; mod custom_payload; mod interact; mod keep_alive; +mod move_vehicle; +mod paddle_boat; mod pick_item; mod ping_request; mod player_abilities; @@ -48,6 +50,8 @@ pub use cookie_response::*; pub use custom_payload::*; pub use interact::*; pub use keep_alive::*; +pub use move_vehicle::*; +pub use paddle_boat::*; pub use pick_item::*; pub use ping_request::*; pub use player_abilities::*; diff --git a/pumpkin-protocol/src/java/server/play/move_vehicle.rs b/pumpkin-protocol/src/java/server/play/move_vehicle.rs new file mode 100644 index 000000000..d4ab59da6 --- /dev/null +++ b/pumpkin-protocol/src/java/server/play/move_vehicle.rs @@ -0,0 +1,13 @@ +use pumpkin_data::packet::serverbound::PLAY_MOVE_VEHICLE; +use pumpkin_macros::java_packet; +use serde::Deserialize; + +#[derive(Deserialize)] +#[java_packet(PLAY_MOVE_VEHICLE)] +pub struct SMoveVehicle { + pub x: f64, + pub y: f64, + pub z: f64, + pub yaw: f32, + pub pitch: f32, +} diff --git a/pumpkin-protocol/src/java/server/play/paddle_boat.rs b/pumpkin-protocol/src/java/server/play/paddle_boat.rs new file mode 100644 index 000000000..284b0ea64 --- /dev/null +++ b/pumpkin-protocol/src/java/server/play/paddle_boat.rs @@ -0,0 +1,10 @@ +use pumpkin_data::packet::serverbound::PLAY_PADDLE_BOAT; +use pumpkin_macros::java_packet; +use serde::Deserialize; + +#[derive(Deserialize)] +#[java_packet(PLAY_PADDLE_BOAT)] +pub struct SPaddleBoat { + pub left_paddle: bool, + pub right_paddle: bool, +} diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 6f0c718b3..c80b60758 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -27,10 +27,11 @@ use pumpkin_data::{ use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::{ + PositionFlag, codec::var_int::VarInt, java::client::play::{ - CEntityPositionSync, CEntityVelocity, CHeadRot, CSetEntityMetadata, CSpawnEntity, - CUpdateEntityRot, Metadata, + CEntityPositionSync, CEntityVelocity, CHeadRot, CPlayerPosition, CSetEntityMetadata, + CSetPassengers, CSpawnEntity, CUpdateEntityRot, Metadata, }, }; use pumpkin_util::math::vector3::Axis; @@ -76,6 +77,7 @@ pub mod projectile; pub mod projectile_deflection; pub mod tnt; pub mod r#type; +pub mod vehicle; mod combat; pub mod predicate; @@ -234,6 +236,10 @@ pub trait EntityBase: Send + Sync + NBTStorage { Box::pin(async {}) } + fn set_paddle_state(&self, _left: bool, _right: bool) -> EntityBaseFuture<'_, ()> { + Box::pin(async {}) + } + fn get_entity(&self) -> &Entity; fn get_living_entity(&self) -> Option<&LivingEntity>; @@ -401,6 +407,8 @@ pub struct Entity { pub passengers: Mutex>>, /// The vehicle that entity is in pub vehicle: Mutex>>, + /// Cooldown before entity can mount again after dismounting + pub riding_cooldown: AtomicI32, /// The age of the entity in ticks. Negative values indicate a baby. pub age: AtomicI32, @@ -497,6 +505,7 @@ impl Entity { removal_reason: AtomicCell::new(None), passengers: Mutex::new(Vec::new()), vehicle: Mutex::new(None), + riding_cooldown: AtomicI32::new(0), age: AtomicI32::new(0), portal_cooldown: AtomicU32::new(0), portal_manager: Mutex::new(None), @@ -2102,6 +2111,211 @@ impl Entity { vehicle.is_some() } + pub async fn add_passenger( + &self, + vehicle: Arc, + passenger: Arc, + ) { + let passenger_entity = passenger.get_entity(); + *passenger_entity.vehicle.lock().await = Some(vehicle); + + let mut passengers = self.passengers.lock().await; + passengers.push(passenger); + + let passenger_ids: Vec = passengers + .iter() + .map(|p| VarInt(p.get_entity().entity_id)) + .collect(); + + let world = self.world.load(); + world + .broadcast_packet_all(&CSetPassengers::new(VarInt(self.entity_id), &passenger_ids)) + .await; + } + + #[allow(clippy::too_many_lines)] + pub async fn remove_passenger(&self, passenger_id: i32) { + let mut passengers = self.passengers.lock().await; + let removed_passenger = if let Some(idx) = passengers + .iter() + .position(|p| p.get_entity().entity_id == passenger_id) + { + let passenger = passengers.remove(idx); + *passenger.get_entity().vehicle.lock().await = None; + Some(passenger) + } else { + None + }; + + let passenger_ids: Vec = passengers + .iter() + .map(|p| VarInt(p.get_entity().entity_id)) + .collect(); + drop(passengers); + + if let Some(passenger) = removed_passenger { + let vehicle_box = self.bounding_box.load(); + let passenger_entity = passenger.get_entity(); + let passenger_yaw = passenger_entity.yaw.load(); + let passenger_width = passenger_entity.entity_dimension.load().width as f64; + let vehicle_width = self.entity_dimension.load().width as f64; + + // Pre-allocate teleport ID and block movement packets BEFORE sending + // CSetPassengers. This prevents a race condition where the client receives + // the dismount packet, sends stale position packets from the old riding + // position, and the server processes them before the teleport arrives. + let teleport_id = if let Some(player) = passenger.get_player() { + let id = player + .teleport_id_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + + 1; + // Use fallback position as placeholder — updated below with real position + let placeholder = + Vector3::new(self.pos.load().x, vehicle_box.max.y, self.pos.load().z); + *player.awaiting_teleport.lock().await = Some((id.into(), placeholder)); + Some(id) + } else { + None + }; + + // Vanilla: ridingCooldown = 60 (prevents immediate re-mount) + passenger_entity.riding_cooldown.store(60, Relaxed); + // TODO: world.emitGameEvent(passenger, GameEvent.ENTITY_DISMOUNT, vehicle.pos) + + // Now send CSetPassengers — client movement is already blocked. + // Vanilla sends this directly to the dismounting player's connection, + // then broadcasts to other players separately. + let world = self.world.load(); + let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids); + if let Some(player) = passenger.get_player() { + player.client.enqueue_packet(&passengers_packet).await; + world + .broadcast_packet_except(&[player.gameprofile.id], &passengers_packet) + .await; + } else { + world.broadcast_packet_all(&passengers_packet).await; + } + + // Calculate dismount offset (vanilla getPassengerDismountOffset) + let offset_dist = + (vehicle_width * std::f64::consts::SQRT_2 + passenger_width + 0.00001) / 2.0; + let yaw_rad = (-passenger_yaw).to_radians(); + let sin_yaw = f64::from(yaw_rad.sin()); + let cos_yaw = f64::from(yaw_rad.cos()); + let max_component = sin_yaw.abs().max(cos_yaw.abs()); + let offset_x = sin_yaw * offset_dist / max_component; + let offset_z = cos_yaw * offset_dist / max_component; + + let target_x = self.pos.load().x + offset_x; + let target_z = self.pos.load().z + offset_z; + let target_block_y = vehicle_box.max.y.floor() as i32; + let block_pos = BlockPos(Vector3::new( + target_x.floor() as i32, + target_block_y, + target_z.floor() as i32, + )); + let below_pos = BlockPos(Vector3::new( + target_x.floor() as i32, + target_block_y - 1, + target_z.floor() as i32, + )); + + let below_state_id = world.get_block_state_id(&below_pos).await; + // Vanilla: isWater checks specifically for water fluid, not any fluid + let is_water = Fluid::from_state_id(below_state_id) + .is_some_and(|f| f.id == Fluid::WATER.id || f.id == Fluid::FLOWING_WATER.id); + + let fallback_pos = + Vector3::new(self.pos.load().x, vehicle_box.max.y, self.pos.load().z); + + let dismount_pos = if is_water { + fallback_pos + } else { + // Vanilla tries two Y levels: at vehicle top and one block below + let mut candidates = Vec::new(); + for pos in [&block_pos, &below_pos] { + let height = world.get_dismount_height(pos).await; + // Vanilla: canDismountInBlock = !height.is_infinite() && height < 1.0 + if height.is_finite() && height < 1.0 { + candidates.push(Vector3::new( + target_x, + f64::from(pos.0.y) + height, + target_z, + )); + } + } + + // Try poses: Standing, Crouching, Swimming (vanilla order) + let poses = [ + EntityPose::Standing, + EntityPose::Crouching, + EntityPose::Swimming, + ]; + let mut found = None; + 'outer: for pose in poses { + let dims = Self::get_entity_dimensions(pose); + for candidate in &candidates { + let bbox = + BoundingBox::new_from_pos(candidate.x, candidate.y, candidate.z, &dims); + if world.is_space_empty(bbox).await { + found = Some((*candidate, pose)); + break 'outer; + } + } + } + + if let Some((pos, pose)) = found { + if pose != EntityPose::Standing { + passenger_entity.set_pose(pose).await; + } + pos + } else { + fallback_pos + } + }; + + if let Some(player) = passenger.get_player() { + let id = teleport_id.unwrap(); + player.living_entity.entity.set_pos(dismount_pos); + // Update awaiting_teleport with the real dismount position + *player.awaiting_teleport.lock().await = Some((id.into(), dismount_pos)); + // Use enqueue_packet (not send_packet_now) so the teleport goes through + // the same packet queue as CSetPassengers, preserving send order. + // Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative. + // With rotation relative and yaw/pitch=0, the client preserves its current look. + player + .client + .enqueue_packet(&CPlayerPosition::new( + id.into(), + dismount_pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 0.0, + vec![ + PositionFlag::DeltaX, + PositionFlag::DeltaY, + PositionFlag::DeltaZ, + PositionFlag::YRot, + PositionFlag::XRot, + ], + )) + .await; + // Vanilla: setSneaking(false) after dismount via sneak input + if passenger_entity.sneaking.load(Relaxed) { + passenger_entity.set_sneaking(false).await; + } + } else { + passenger_entity.set_pos(dismount_pos); + } + } else { + // No passenger was removed, still need to broadcast the passenger list + let world = self.world.load(); + world + .broadcast_packet_all(&CSetPassengers::new(VarInt(self.entity_id), &passenger_ids)) + .await; + } + } + pub async fn check_out_of_world(&self, dyn_self: &dyn EntityBase) { if self.pos.load().y < f64::from(self.world.load().dimension.min_y) - 64.0 { dyn_self.tick_in_void(dyn_self).await; @@ -2257,6 +2471,12 @@ impl EntityBase for Entity { // Tick freeze state (powder snow) self.tick_frozen(&*caller).await; + + let riding_cooldown = self.riding_cooldown.load(Ordering::Relaxed); + if riding_cooldown > 0 { + self.riding_cooldown + .store(riding_cooldown - 1, Ordering::Relaxed); + } }) } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 7b1fe2c23..d35d2b708 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -730,9 +730,10 @@ impl Player { return; } + player_attack_sound(&pos, &world, attack_type).await; + if victim.get_living_entity().is_some() { let mut knockback_strength = 1.0; - player_attack_sound(&pos, &world, attack_type).await; match attack_type { AttackType::Knockback => knockback_strength += 1.0, AttackType::Sweeping => { diff --git a/pumpkin/src/entity/vehicle/boat.rs b/pumpkin/src/entity/vehicle/boat.rs new file mode 100644 index 000000000..de5063f05 --- /dev/null +++ b/pumpkin/src/entity/vehicle/boat.rs @@ -0,0 +1,242 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +use crossbeam::atomic::AtomicCell; + +use crate::entity::player::Player; +use crate::entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity}; +use crate::server::Server; +use crate::world::loot::{LootContextParameters, LootTableExt}; +use pumpkin_data::damage::DamageType; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::GameMode; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::item::ItemStack; + +pub struct BoatEntity { + entity: Entity, + damage_wobble_ticks: AtomicI32, + damage_wobble_side: AtomicI32, + damage_wobble_strength: AtomicCell, + ticks_underwater: AtomicCell, + left_paddle_moving: AtomicBool, + right_paddle_moving: AtomicBool, +} + +impl BoatEntity { + pub const fn new(entity: Entity) -> Self { + Self { + entity, + damage_wobble_ticks: AtomicI32::new(0), + damage_wobble_side: AtomicI32::new(1), + damage_wobble_strength: AtomicCell::new(0.0), + ticks_underwater: AtomicCell::new(0.0), + left_paddle_moving: AtomicBool::new(false), + right_paddle_moving: AtomicBool::new(false), + } + } + + pub async fn set_paddles(&self, left: bool, right: bool) { + self.left_paddle_moving.store(left, Ordering::Relaxed); + self.right_paddle_moving.store(right, Ordering::Relaxed); + + self.entity + .send_meta_data(&[ + Metadata::new( + TrackedData::DATA_LEFT_PADDLE_MOVING, + MetaDataType::Boolean, + left, + ), + Metadata::new( + TrackedData::DATA_RIGHT_PADDLE_MOVING, + MetaDataType::Boolean, + right, + ), + ]) + .await; + } + + async fn send_wobble_metadata(&self) { + self.entity + .send_meta_data(&[ + Metadata::new( + TrackedData::DATA_DAMAGE_WOBBLE_TICKS, + MetaDataType::Integer, + VarInt(self.damage_wobble_ticks.load(Ordering::Relaxed)), + ), + Metadata::new( + TrackedData::DATA_DAMAGE_WOBBLE_SIDE, + MetaDataType::Integer, + VarInt(self.damage_wobble_side.load(Ordering::Relaxed)), + ), + ]) + .await; + self.entity + .send_meta_data(&[Metadata::new( + TrackedData::DATA_DAMAGE_WOBBLE_STRENGTH, + MetaDataType::Float, + self.damage_wobble_strength.load(), + )]) + .await; + } + + async fn kill_and_drop_self(&self) { + let world = self.entity.world.load(); + let entity_drops = world.level_info.load().game_rules.entity_drops; + + if entity_drops && let Some(loot_table) = &self.entity.entity_type.loot_table { + let pos = self.entity.block_pos.load(); + let params = LootContextParameters::default(); + for stack in loot_table.get_loot(params) { + world.drop_stack(&pos, stack).await; + } + } + + self.entity.remove().await; + } +} + +impl NBTStorage for BoatEntity {} + +impl EntityBase for BoatEntity { + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn tick<'a>( + &'a self, + _caller: Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let ticks = self.damage_wobble_ticks.load(Ordering::Relaxed); + if ticks > 0 { + self.damage_wobble_ticks.store(ticks - 1, Ordering::Relaxed); + } + + let strength = self.damage_wobble_strength.load(); + if strength > 0.0 { + self.damage_wobble_strength.store(strength - 1.0); + } + + let underwater = self.ticks_underwater.load(); + if self.entity.touching_water.load(Ordering::Relaxed) { + self.ticks_underwater.store((underwater + 1.0).min(60.0)); + } else if underwater > 0.0 { + self.ticks_underwater.store((underwater - 1.0).max(0.0)); + } + }) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.send_wobble_metadata().await; + }) + } + + fn can_hit(&self) -> bool { + self.entity.is_alive() + } + + fn is_collidable(&self, _entity: Option>) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + amount: f32, + _damage_type: DamageType, + _position: Option>, + source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if !self.entity.is_alive() { + return true; + } + + let current_side = self.damage_wobble_side.load(Ordering::Relaxed); + self.damage_wobble_side + .store(-current_side, Ordering::Relaxed); + self.damage_wobble_ticks.store(10, Ordering::Relaxed); + self.entity.velocity_dirty.store(true, Ordering::SeqCst); + + let current_strength = self.damage_wobble_strength.load(); + let new_strength = current_strength + amount * 10.0; + self.damage_wobble_strength.store(new_strength); + + self.send_wobble_metadata().await; + + let is_creative = source + .and_then(|s| s.get_player()) + .is_some_and(|p| p.gamemode.load() == GameMode::Creative); + + if is_creative || new_strength > 40.0 { + if is_creative { + self.entity.remove().await; + } else { + self.kill_and_drop_self().await; + } + } + + true + }) + } + + fn interact<'a>( + &'a self, + player: &'a Player, + _item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if player.living_entity.entity.sneaking.load(Ordering::Relaxed) { + return false; + } + + if self.ticks_underwater.load() >= 60.0 { + return false; + } + + if self.entity.passengers.lock().await.len() >= 2 { + return false; + } + + if player.living_entity.entity.has_vehicle().await { + return false; + } + + let world = self.entity.world.load(); + let Some(vehicle) = world.get_entity_by_id(self.entity.entity_id) else { + return false; + }; + + let Some(passenger) = world.get_player_by_id(player.entity_id()) else { + return false; + }; + + self.entity + .add_passenger(vehicle, passenger as Arc) + .await; + + true + }) + } + + fn set_paddle_state(&self, left: bool, right: bool) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.set_paddles(left, right).await; + }) + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } +} diff --git a/pumpkin/src/entity/vehicle/mod.rs b/pumpkin/src/entity/vehicle/mod.rs new file mode 100644 index 000000000..68d7f1ab2 --- /dev/null +++ b/pumpkin/src/entity/vehicle/mod.rs @@ -0,0 +1 @@ +pub mod boat; diff --git a/pumpkin/src/item/items/boat.rs b/pumpkin/src/item/items/boat.rs new file mode 100644 index 000000000..e852eecd7 --- /dev/null +++ b/pumpkin/src/item/items/boat.rs @@ -0,0 +1,193 @@ +use std::pin::Pin; +use std::sync::Arc; + +use crate::entity::Entity; +use crate::entity::player::Player; +use crate::entity::vehicle::boat::BoatEntity; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::Block; +use pumpkin_data::entity::EntityType; +use pumpkin_data::fluid::Fluid; +use pumpkin_data::item::Item; +use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +use crate::world::World; + +pub struct BoatItem; + +impl BoatItem { + /// Maps boat item to corresponding entity type + fn item_to_entity(item: &Item) -> &'static EntityType { + match item.id { + val if val == Item::OAK_BOAT.id => &EntityType::OAK_BOAT, + val if val == Item::OAK_CHEST_BOAT.id => &EntityType::OAK_CHEST_BOAT, + val if val == Item::SPRUCE_BOAT.id => &EntityType::SPRUCE_BOAT, + val if val == Item::SPRUCE_CHEST_BOAT.id => &EntityType::SPRUCE_CHEST_BOAT, + val if val == Item::BIRCH_BOAT.id => &EntityType::BIRCH_BOAT, + val if val == Item::BIRCH_CHEST_BOAT.id => &EntityType::BIRCH_CHEST_BOAT, + val if val == Item::JUNGLE_BOAT.id => &EntityType::JUNGLE_BOAT, + val if val == Item::JUNGLE_CHEST_BOAT.id => &EntityType::JUNGLE_CHEST_BOAT, + val if val == Item::ACACIA_BOAT.id => &EntityType::ACACIA_BOAT, + val if val == Item::ACACIA_CHEST_BOAT.id => &EntityType::ACACIA_CHEST_BOAT, + val if val == Item::DARK_OAK_BOAT.id => &EntityType::DARK_OAK_BOAT, + val if val == Item::DARK_OAK_CHEST_BOAT.id => &EntityType::DARK_OAK_CHEST_BOAT, + val if val == Item::MANGROVE_BOAT.id => &EntityType::MANGROVE_BOAT, + val if val == Item::MANGROVE_CHEST_BOAT.id => &EntityType::MANGROVE_CHEST_BOAT, + val if val == Item::CHERRY_BOAT.id => &EntityType::CHERRY_BOAT, + val if val == Item::CHERRY_CHEST_BOAT.id => &EntityType::CHERRY_CHEST_BOAT, + val if val == Item::PALE_OAK_BOAT.id => &EntityType::PALE_OAK_BOAT, + val if val == Item::PALE_OAK_CHEST_BOAT.id => &EntityType::PALE_OAK_CHEST_BOAT, + val if val == Item::BAMBOO_RAFT.id => &EntityType::BAMBOO_RAFT, + val if val == Item::BAMBOO_CHEST_RAFT.id => &EntityType::BAMBOO_CHEST_RAFT, + _ => unreachable!(), + } + } + + /// Gets entity dimensions for the boat type + const fn get_entity_dimensions(entity_type: &EntityType) -> EntityDimensions { + EntityDimensions::new( + entity_type.dimension[0], + entity_type.dimension[1], + entity_type.eye_height, + ) + } +} + +impl ItemMetadata for BoatItem { + fn ids() -> Box<[u16]> { + [ + Item::OAK_BOAT.id, + Item::OAK_CHEST_BOAT.id, + Item::SPRUCE_BOAT.id, + Item::SPRUCE_CHEST_BOAT.id, + Item::BIRCH_BOAT.id, + Item::BIRCH_CHEST_BOAT.id, + Item::JUNGLE_BOAT.id, + Item::JUNGLE_CHEST_BOAT.id, + Item::ACACIA_BOAT.id, + Item::ACACIA_CHEST_BOAT.id, + Item::DARK_OAK_BOAT.id, + Item::DARK_OAK_CHEST_BOAT.id, + Item::MANGROVE_BOAT.id, + Item::MANGROVE_CHEST_BOAT.id, + Item::CHERRY_BOAT.id, + Item::CHERRY_CHEST_BOAT.id, + Item::PALE_OAK_BOAT.id, + Item::PALE_OAK_CHEST_BOAT.id, + Item::BAMBOO_RAFT.id, + Item::BAMBOO_CHEST_RAFT.id, + ] + .into() + } +} + +impl ItemBehaviour for BoatItem { + /// Vanilla: `BoatItem.use()` - raycasts to find placement position + fn normal_use<'a>( + &'a self, + item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + let (start_pos, end_pos) = self.get_start_and_end_pos(player); + + // Vanilla: raycast with FluidHandling.ANY - stops on water/lava surface or solid blocks + let checker = async |pos: &BlockPos, world_inner: &Arc| { + let state_id = world_inner.get_block_state_id(pos).await; + + // Air doesn't stop the raycast + if state_id == Block::AIR.id { + return false; + } + + // Check if it's a fluid - stop on any fluid (FluidHandling.ANY in vanilla) + if Fluid::from_state_id(state_id).is_some() { + return true; + } + + // Stop on solid blocks + true + }; + + let Some((hit_pos, _direction)) = world.raycast(start_pos, end_pos, checker).await + else { + return; + }; + + // Calculate hit position - center of the block top or water surface + // TODO: Vanilla uses exact raycast intersection point (hitResult.getPos()), + // Pumpkin's raycast only returns block positions. + let hit_vec = Vector3::new( + f64::from(hit_pos.0.x) + 0.5, + f64::from(hit_pos.0.y) + 1.0, + f64::from(hit_pos.0.z) + 0.5, + ); + + // Vanilla: Check for entities in the path that would block placement + // Get player's rotation vector stretched by 5.0 and expanded by 1.0 + let (yaw, pitch) = player.rotation(); + let rotation_vec = + Vector3::rotation_vector(f64::from(pitch), f64::from(yaw)).multiply(5.0, 5.0, 5.0); + let player_entity = player.living_entity.entity.bounding_box.load(); + let search_box = player_entity.stretch(rotation_vec).expand_all(1.0); + + let entities = world.get_entities_at_box(&search_box); + let player_eye_pos = player.eye_position(); + + for entity in &entities { + // Vanilla: EntityPredicates.CAN_HIT = !isSpectator() && canHit() + if entity.is_spectator() || !entity.can_hit() { + continue; + } + + // Vanilla: expand by getTargetingMargin() (0.0 for most entities) + let entity_box = entity.get_entity().bounding_box.load(); + // Check if entity's bounding box contains the player's eye position + if entity_box.intersects(&BoundingBox::new(player_eye_pos, player_eye_pos)) { + // Entity is blocking the line of sight at eye position + return; + } + } + + // Create the boat entity + let entity_type = Self::item_to_entity(item); + let dimensions = Self::get_entity_dimensions(entity_type); + let boat_box = BoundingBox::new_from_pos(hit_vec.x, hit_vec.y, hit_vec.z, &dimensions); + + // Vanilla: if (!world.isSpaceEmpty(lv7, lv7.getBoundingBox())) return FAIL + if !world.is_space_empty(boat_box).await { + return; + } + + // Check no entities occupy the space + if !world.get_entities_at_box(&boat_box).is_empty() { + return; + } + + // Create and spawn the boat + let entity = Entity::new(world.clone(), hit_vec, entity_type); + + // Set yaw to player's yaw + let (player_yaw, _) = player.rotation(); + entity.set_rotation(player_yaw, 0.0); + + let boat_entity = Arc::new(BoatEntity::new(entity)); + world.spawn_entity(boat_entity).await; + + // Decrement item unless in creative mode + let held_item = player.inventory.held_item(); + let mut stack = held_item.lock().await; + stack.decrement_unless_creative(player.gamemode.load(), 1); + + // TODO: world.emitGameEvent(user, GameEvent.ENTITY_PLACE, hitResult.getPos()) + // TODO: user.incrementStat(Stats.USED.getOrCreateStat(this)) + }) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index 94cdd3a50..c920168cf 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -1,5 +1,6 @@ pub mod armor_stand; pub mod axe; +pub mod boat; pub mod bucket; pub mod dye; pub mod egg; @@ -22,6 +23,7 @@ pub mod trident; pub mod wind_charge; use crate::item::items::armor_stand::ArmorStandItem; +use crate::item::items::boat::BoatItem; use crate::item::items::end_crystal::EndCrystalItem; use crate::item::items::firework_rocket::FireworkRocketItem; use crate::item::items::minecart::MinecartItem; @@ -76,6 +78,7 @@ pub fn default_registry() -> Arc { manager.register(GlowingInkSacItem); manager.register(ArmorStandItem); manager.register(WindChargeItem); + manager.register(BoatItem); Arc::new(manager) } diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index 3d04b7827..d79a2d322 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -8,10 +8,11 @@ use pumpkin_data::packet::CURRENT_MC_PROTOCOL; use pumpkin_protocol::java::server::play::{ SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport, - SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SKeepAlive, SPickItemFromBlock, - SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, - SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock, - SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, + SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SKeepAlive, SMoveVehicle, + SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction, + SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, + SPlayerRotation, SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, + SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, }; use pumpkin_protocol::packet::MultiVersionJavaPacket; use pumpkin_protocol::{ @@ -687,6 +688,14 @@ impl JavaClient { self.handle_player_input(player, SPlayerInput::read(payload)?) .await; } + id if id == SMoveVehicle::PACKET_ID => { + self.handle_move_vehicle(player, SMoveVehicle::read(payload)?) + .await; + } + id if id == SPaddleBoat::PACKET_ID => { + self.handle_paddle_boat(player, SPaddleBoat::read(payload)?) + .await; + } id if id == SInteract::PACKET_ID => { self.handle_interact(player, SInteract::read(payload)?, server) .await; diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index ec4172067..658e06a54 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -51,10 +51,10 @@ use pumpkin_protocol::java::server::play::{ Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, SCookieResponse as SPCookieResponse, SInteract, - SKeepAlive, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction, - SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, - SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, - SUpdateSign, SUseItem, SUseItemOn, Status, + SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, + SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, + SPlayerRotation, SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, + SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status, }; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::vector3::Vector3; @@ -294,6 +294,10 @@ impl JavaClient { if !player.has_client_loaded() { return; } + // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) + if player.awaiting_teleport.lock().await.is_some() { + return; + } // y = feet Y let position = packet.position; if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() { @@ -402,6 +406,10 @@ impl JavaClient { if !player.has_client_loaded() { return; } + // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) + if player.awaiting_teleport.lock().await.is_some() { + return; + } // y = feet Y let position = packet.position; if !position.x.is_finite() @@ -813,6 +821,35 @@ impl JavaClient { if player.get_entity().sneaking.load(Ordering::Relaxed) != sneak { player.get_entity().set_sneaking(sneak).await; } + + if sneak { + let vehicle = player.get_entity().vehicle.lock().await.clone(); + if let Some(vehicle) = vehicle { + vehicle + .get_entity() + .remove_passenger(player.entity_id()) + .await; + } + } + } + + pub async fn handle_move_vehicle(&self, player: &Arc, packet: SMoveVehicle) { + let entity = player.get_entity(); + let vehicle = entity.vehicle.lock().await; + if let Some(vehicle) = vehicle.as_ref() { + let vehicle_entity = vehicle.get_entity(); + vehicle_entity.set_pos(Vector3::new(packet.x, packet.y, packet.z)); + vehicle_entity.set_rotation(packet.yaw, packet.pitch); + } + } + + pub async fn handle_paddle_boat(&self, player: &Arc, packet: SPaddleBoat) { + let vehicle = player.get_entity().vehicle.lock().await.clone(); + if let Some(vehicle) = vehicle { + vehicle + .set_paddle_state(packet.left_paddle, packet.right_paddle) + .await; + } } pub async fn handle_swing_arm(&self, player: &Arc, swing_arm: SSwingArm) { diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index ad303b8ef..2774f12dd 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1170,6 +1170,32 @@ impl World { true } + /// Vanilla's `BlockView.getDismountHeight()`. + /// Returns the Y surface height for dismounting at the given block position, + /// or `f64::NEG_INFINITY` if no valid surface exists. + pub async fn get_dismount_height(&self, pos: &BlockPos) -> f64 { + let state = self.get_block_state(pos).await; + let max_y = state + .get_block_collision_shapes() + .map(|s| s.max.y) + .fold(f64::NEG_INFINITY, f64::max); + if max_y != f64::NEG_INFINITY { + return max_y; + } + // No collision at pos — check block below + let below = BlockPos(Vector3::new(pos.0.x, pos.0.y - 1, pos.0.z)); + let below_state = self.get_block_state(&below).await; + let below_max_y = below_state + .get_block_collision_shapes() + .map(|s| s.max.y) + .fold(f64::NEG_INFINITY, f64::max); + if below_max_y >= 1.0 { + below_max_y - 1.0 + } else { + f64::NEG_INFINITY + } + } + pub async fn tick_spawning_chunk( self: &Arc, chunk_pos: Vector2,