mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: complete boat implementation (#1440)
* feat: implement boat placement - add boat item handler for all boat/raft types - raycast with fluid handling to place on water or blocks - filter entities with can_hit predicate (vanilla parity) - check space is empty before spawning - decrement item unless creative mode * feat: add boat entity with damage handling - create BoatEntity struct implementing EntityBase - implement can_hit() and is_collidable() for boats - implement damage handling that drops boat item on destroy - update boat item to spawn BoatEntity instead of generic Entity * fix: boat damage wobble and attack sounds match vanilla Boats now accumulate damage with wobble animation that decays over time. Attack sounds play for all hittable entities, not just living entities. * feat: add boat riding and vehicle movement packets * feat: complete boat implementation matching vanilla - add underwater check (60 ticks) and max 2 passengers limit - add paddle animation metadata and handler - implement proper land dismount with offset calculation - add riding_cooldown field to entity * fix: clippy warnings for boat implementation * refactor: use loot tables for boat item drops * fix: collapse nested if to satisfy clippy * style: fix cargo fmt formatting for collapsible if * fix: boat dismount places player correctly beside vehicle - implement vanilla's dismount position calculation with getDismountHeight, canDismountInBlock, and pose fallback (standing → crouching → swimming) - fix packet ordering race: use enqueue_packet for teleport so it arrives after CSetPassengers (send_packet_now bypassed the queue) - pre-set awaiting_teleport before CSetPassengers to block stale movement packets during async dismount calculation - send CSetPassengers directly to dismounting player, broadcast to others (matching vanilla's targeted packet behavior) - use DELTA|ROT position flags for teleport (vanilla preserves current look direction with relative rotation) - add riding cooldown (60 ticks) to prevent immediate re-mount - reset sneaking state after player dismount - fix water detection to check specifically for water/flowing_water fluids (Fluid::from_state_id returns EMPTY for air blocks) - revert broken loot table refactor for boat item drops (boats use hardcoded entity-to-item mapping, not loot tables) - add get_dismount_height helper on World matching vanilla's BlockView.getDismountHeight() - ignore movement packets while awaiting teleport confirmation in handle_position and handle_position_rotation * style: fix cargo fmt formatting * refactor: use loot tables for boat item drops add loot table entries to entities.json for all 20 boat/raft types and use the existing loot table system instead of hardcoded mapping. * fix: resolve rebase conflicts and clippy warnings * fix: regenerate codegen to include boat loot tables
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<u8> {
|
||||
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
|
||||
|
||||
@@ -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<Self> {
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
22
pumpkin-protocol/src/java/client/play/set_passengers.rs
Normal file
22
pumpkin-protocol/src/java/client/play/set_passengers.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
13
pumpkin-protocol/src/java/server/play/move_vehicle.rs
Normal file
13
pumpkin-protocol/src/java/server/play/move_vehicle.rs
Normal file
@@ -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,
|
||||
}
|
||||
10
pumpkin-protocol/src/java/server/play/paddle_boat.rs
Normal file
10
pumpkin-protocol/src/java/server/play/paddle_boat.rs
Normal file
@@ -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,
|
||||
}
|
||||
@@ -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<Vec<Arc<dyn EntityBase>>>,
|
||||
/// The vehicle that entity is in
|
||||
pub vehicle: Mutex<Option<Arc<dyn EntityBase>>>,
|
||||
/// 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<dyn EntityBase>,
|
||||
passenger: Arc<dyn EntityBase>,
|
||||
) {
|
||||
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<VarInt> = 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<VarInt> = 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
242
pumpkin/src/entity/vehicle/boat.rs
Normal file
242
pumpkin/src/entity/vehicle/boat.rs
Normal file
@@ -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<f32>,
|
||||
ticks_underwater: AtomicCell<f32>,
|
||||
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<dyn EntityBase>,
|
||||
_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<Box<dyn EntityBase>>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn damage_with_context<'a>(
|
||||
&'a self,
|
||||
_caller: &'a dyn EntityBase,
|
||||
amount: f32,
|
||||
_damage_type: DamageType,
|
||||
_position: Option<Vector3<f64>>,
|
||||
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<dyn EntityBase>)
|
||||
.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
|
||||
}
|
||||
}
|
||||
1
pumpkin/src/entity/vehicle/mod.rs
Normal file
1
pumpkin/src/entity/vehicle/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod boat;
|
||||
193
pumpkin/src/item/items/boat.rs
Normal file
193
pumpkin/src/item/items/boat.rs
Normal file
@@ -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<Box<dyn Future<Output = ()> + 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<World>| {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<ItemRegistry> {
|
||||
manager.register(GlowingInkSacItem);
|
||||
manager.register(ArmorStandItem);
|
||||
manager.register(WindChargeItem);
|
||||
manager.register(BoatItem);
|
||||
|
||||
Arc::new(manager)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Player>, 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<Player>, 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<Player>, swing_arm: SSwingArm) {
|
||||
|
||||
@@ -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<Self>,
|
||||
chunk_pos: Vector2<i32>,
|
||||
|
||||
Reference in New Issue
Block a user