|
| 1 | +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; |
| 2 | +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; |
| 3 | +use crate::command::argument_types::core::string::StringArgumentType; |
| 4 | +use crate::command::argument_types::entity::EntityArgumentType; |
| 5 | +use crate::command::context::command_context::CommandContext; |
| 6 | +use crate::command::errors::error_types::CommandErrorType; |
| 7 | +use crate::command::node::dispatcher::CommandDispatcher; |
| 8 | +use crate::command::node::{CommandExecutor, CommandExecutorResult}; |
| 9 | +use crate::world::loot::LootTableExt; |
| 10 | +use pumpkin_data::translation; |
| 11 | +use pumpkin_util::PermissionLvl; |
| 12 | +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; |
| 13 | +use pumpkin_util::text::TextComponent; |
| 14 | + |
| 15 | +const DESCRIPTION: &str = |
| 16 | + "Drops the given loot table into the specified inventory or into the world."; |
| 17 | +const PERMISSION: &str = "minecraft:command.loot"; |
| 18 | + |
| 19 | +static ERROR_INVALID_LOOT_TABLE: CommandErrorType<1> = CommandErrorType::new( |
| 20 | + translation::bedrock::COMMANDS_LOOT_FAILURE_INVALIDLOOTTABLE, |
| 21 | + "Loot table '%s' not found", |
| 22 | +); |
| 23 | + |
| 24 | +static ERROR_ENTITY_NO_LOOT_TABLE: CommandErrorType<1> = CommandErrorType::new( |
| 25 | + translation::bedrock::COMMANDS_LOOT_FAILURE_ENTITYNOLOOTTABLE, |
| 26 | + "Entity %s has no loot table", |
| 27 | +); |
| 28 | + |
| 29 | +static ERROR_NO_CONTAINER: CommandErrorType<1> = CommandErrorType::new( |
| 30 | + translation::bedrock::COMMANDS_LOOT_FAILURE_NOCONTAINER, |
| 31 | + "Target position %s is not a container", |
| 32 | +); |
| 33 | + |
| 34 | +#[derive(Clone, Copy)] |
| 35 | +enum Target { |
| 36 | + Give, |
| 37 | + Spawn, |
| 38 | + Insert, |
| 39 | +} |
| 40 | + |
| 41 | +#[derive(Clone, Copy)] |
| 42 | +enum Source { |
| 43 | + Loot, |
| 44 | + Kill, |
| 45 | + Mine { has_tool: bool }, |
| 46 | +} |
| 47 | + |
| 48 | +struct LootExecutor { |
| 49 | + target: Target, |
| 50 | + source: Source, |
| 51 | +} |
| 52 | + |
| 53 | +async fn insert_into_inventory( |
| 54 | + inventory: &dyn pumpkin_world::inventory::Inventory, |
| 55 | + mut stack: pumpkin_data::item_stack::ItemStack, |
| 56 | +) -> pumpkin_data::item_stack::ItemStack { |
| 57 | + for i in 0..inventory.size() { |
| 58 | + if stack.is_empty() { |
| 59 | + break; |
| 60 | + } |
| 61 | + let slot = inventory.get_stack(i).await; |
| 62 | + let mut slot_stack = slot.lock().await; |
| 63 | + if !slot_stack.is_empty() && slot_stack.get_item().id == stack.get_item().id { |
| 64 | + let max_stack_size = 64; |
| 65 | + let space = max_stack_size - slot_stack.item_count; |
| 66 | + if space > 0 { |
| 67 | + let to_add = stack.item_count.min(space); |
| 68 | + slot_stack.item_count += to_add; |
| 69 | + stack.item_count -= to_add; |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + for i in 0..inventory.size() { |
| 75 | + if stack.is_empty() { |
| 76 | + break; |
| 77 | + } |
| 78 | + let slot = inventory.get_stack(i).await; |
| 79 | + let mut slot_stack = slot.lock().await; |
| 80 | + if slot_stack.is_empty() { |
| 81 | + *slot_stack = stack.clone(); |
| 82 | + stack.item_count = 0; |
| 83 | + break; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + stack |
| 88 | +} |
| 89 | + |
| 90 | +impl CommandExecutor for LootExecutor { |
| 91 | + #[allow(clippy::too_many_lines)] |
| 92 | + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { |
| 93 | + Box::pin(async move { |
| 94 | + let mut stacks = Vec::new(); |
| 95 | + |
| 96 | + match self.source { |
| 97 | + Source::Loot => { |
| 98 | + let loot_table_str = StringArgumentType::get(context, "loot_table")?; |
| 99 | + let formatted_key = if loot_table_str.contains(':') { |
| 100 | + loot_table_str.to_string() |
| 101 | + } else { |
| 102 | + format!("minecraft:{loot_table_str}") |
| 103 | + }; |
| 104 | + |
| 105 | + let chest_table = |
| 106 | + pumpkin_data::chest_loot_table::get_chest_loot_table(&formatted_key); |
| 107 | + if let Some(table) = chest_table { |
| 108 | + let seed: i64 = rand::random(); |
| 109 | + stacks = crate::world::loot::generate_chest_loot(table, seed); |
| 110 | + } else { |
| 111 | + return Err(ERROR_INVALID_LOOT_TABLE.create_without_context( |
| 112 | + TextComponent::text(loot_table_str.to_string()), |
| 113 | + )); |
| 114 | + } |
| 115 | + } |
| 116 | + Source::Kill => { |
| 117 | + let target_entities = |
| 118 | + EntityArgumentType::get_entities(context, "target_entity").await?; |
| 119 | + let mut has_loot = false; |
| 120 | + for entity in target_entities { |
| 121 | + if let Some(loot_table) = &entity.get_entity().entity_type.loot_table { |
| 122 | + has_loot = true; |
| 123 | + let params = crate::world::loot::LootContextParameters { |
| 124 | + world_time: context.world().level_info.load().day_time as u64, |
| 125 | + ..Default::default() |
| 126 | + }; |
| 127 | + stacks.extend(loot_table.get_loot(params)); |
| 128 | + } |
| 129 | + } |
| 130 | + if !has_loot { |
| 131 | + let entity_name = "selected entity".to_string(); |
| 132 | + return Err(ERROR_ENTITY_NO_LOOT_TABLE |
| 133 | + .create_without_context(TextComponent::text(entity_name))); |
| 134 | + } |
| 135 | + } |
| 136 | + Source::Mine { has_tool } => { |
| 137 | + let pos = BlockPosArgumentType::get_block_pos(context, "mine_pos") |
| 138 | + .or_else(|_| BlockPosArgumentType::get_block_pos(context, "pos"))?; |
| 139 | + let world = context.world(); |
| 140 | + let block = world.get_block(&pos); |
| 141 | + |
| 142 | + if let Some(loot_table) = &block.loot_table { |
| 143 | + let tool_item = if has_tool { |
| 144 | + let tool_str = StringArgumentType::get(context, "tool")?; |
| 145 | + let key = tool_str.strip_prefix("minecraft:").unwrap_or(tool_str); |
| 146 | + pumpkin_data::item::Item::from_registry_key(key) |
| 147 | + } else { |
| 148 | + None |
| 149 | + }; |
| 150 | + |
| 151 | + let tool_stack = |
| 152 | + tool_item.map(|item| pumpkin_data::item_stack::ItemStack::new(1, item)); |
| 153 | + |
| 154 | + let params = crate::world::loot::LootContextParameters { |
| 155 | + block_state: Some(world.get_block_state(&pos)), |
| 156 | + tool: tool_stack, |
| 157 | + world_time: world.level_info.load().day_time as u64, |
| 158 | + ..Default::default() |
| 159 | + }; |
| 160 | + |
| 161 | + stacks.extend(loot_table.get_loot(params)); |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + let total_items: i32 = stacks.iter().map(|s| s.item_count as i32).sum(); |
| 167 | + |
| 168 | + match self.target { |
| 169 | + Target::Give => { |
| 170 | + let targets = EntityArgumentType::get_players(context, "targets").await?; |
| 171 | + for player in &targets { |
| 172 | + for stack in &stacks { |
| 173 | + let mut remaining = stack.clone(); |
| 174 | + player.inventory.insert_stack_anywhere(&mut remaining).await; |
| 175 | + if !remaining.is_empty() { |
| 176 | + player.drop_item(remaining).await; |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + Target::Spawn => { |
| 182 | + let pos = BlockPosArgumentType::get_block_pos(context, "pos")?; |
| 183 | + let world = context.world(); |
| 184 | + for stack in stacks { |
| 185 | + world.drop_stack(&pos, stack).await; |
| 186 | + } |
| 187 | + } |
| 188 | + Target::Insert => { |
| 189 | + let pos = BlockPosArgumentType::get_block_pos(context, "pos")?; |
| 190 | + let world = context.world(); |
| 191 | + if let Some(block_entity) = world.get_block_entity(&pos) { |
| 192 | + if let Some(inventory) = block_entity.get_inventory() { |
| 193 | + for stack in stacks { |
| 194 | + let remaining = |
| 195 | + insert_into_inventory(inventory.as_ref(), stack).await; |
| 196 | + if !remaining.is_empty() { |
| 197 | + world.drop_stack(&pos, remaining).await; |
| 198 | + } |
| 199 | + } |
| 200 | + } else { |
| 201 | + return Err(ERROR_NO_CONTAINER |
| 202 | + .create_without_context(TextComponent::text(pos.to_string()))); |
| 203 | + } |
| 204 | + } else { |
| 205 | + return Err(ERROR_NO_CONTAINER |
| 206 | + .create_without_context(TextComponent::text(pos.to_string()))); |
| 207 | + } |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + let msg = TextComponent::translate_cross( |
| 212 | + translation::bedrock::COMMANDS_LOOT_SUCCESS, |
| 213 | + translation::bedrock::COMMANDS_LOOT_SUCCESS, |
| 214 | + [TextComponent::text(total_items.to_string())], |
| 215 | + ); |
| 216 | + context.source.send_feedback(msg, true).await; |
| 217 | + |
| 218 | + Ok(total_items) |
| 219 | + }) |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +#[expect(clippy::too_many_lines)] |
| 224 | +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { |
| 225 | + registry.register_permission_or_panic(Permission::new( |
| 226 | + PERMISSION, |
| 227 | + DESCRIPTION, |
| 228 | + PermissionDefault::Op(PermissionLvl::Two), |
| 229 | + )); |
| 230 | + |
| 231 | + let builder = command("loot", DESCRIPTION) |
| 232 | + .requires(PERMISSION) |
| 233 | + // give <targets> |
| 234 | + .then( |
| 235 | + literal("give").then( |
| 236 | + argument("targets", EntityArgumentType::Players) |
| 237 | + .then(literal("loot").then( |
| 238 | + argument("loot_table", StringArgumentType::SingleWord).executes( |
| 239 | + LootExecutor { |
| 240 | + target: Target::Give, |
| 241 | + source: Source::Loot, |
| 242 | + }, |
| 243 | + ), |
| 244 | + )) |
| 245 | + .then(literal("kill").then( |
| 246 | + argument("target_entity", EntityArgumentType::Entities).executes( |
| 247 | + LootExecutor { |
| 248 | + target: Target::Give, |
| 249 | + source: Source::Kill, |
| 250 | + }, |
| 251 | + ), |
| 252 | + )) |
| 253 | + .then( |
| 254 | + literal("mine").then( |
| 255 | + argument("pos", BlockPosArgumentType) |
| 256 | + .executes(LootExecutor { |
| 257 | + target: Target::Give, |
| 258 | + source: Source::Mine { has_tool: false }, |
| 259 | + }) |
| 260 | + .then(argument("tool", StringArgumentType::SingleWord).executes( |
| 261 | + LootExecutor { |
| 262 | + target: Target::Give, |
| 263 | + source: Source::Mine { has_tool: true }, |
| 264 | + }, |
| 265 | + )), |
| 266 | + ), |
| 267 | + ), |
| 268 | + ), |
| 269 | + ) |
| 270 | + // spawn <pos> |
| 271 | + .then( |
| 272 | + literal("spawn").then( |
| 273 | + argument("pos", BlockPosArgumentType) |
| 274 | + .then(literal("loot").then( |
| 275 | + argument("loot_table", StringArgumentType::SingleWord).executes( |
| 276 | + LootExecutor { |
| 277 | + target: Target::Spawn, |
| 278 | + source: Source::Loot, |
| 279 | + }, |
| 280 | + ), |
| 281 | + )) |
| 282 | + .then(literal("kill").then( |
| 283 | + argument("target_entity", EntityArgumentType::Entities).executes( |
| 284 | + LootExecutor { |
| 285 | + target: Target::Spawn, |
| 286 | + source: Source::Kill, |
| 287 | + }, |
| 288 | + ), |
| 289 | + )) |
| 290 | + .then( |
| 291 | + literal("mine").then( |
| 292 | + argument("mine_pos", BlockPosArgumentType) |
| 293 | + .executes(LootExecutor { |
| 294 | + target: Target::Spawn, |
| 295 | + source: Source::Mine { has_tool: false }, |
| 296 | + }) |
| 297 | + .then(argument("tool", StringArgumentType::SingleWord).executes( |
| 298 | + LootExecutor { |
| 299 | + target: Target::Spawn, |
| 300 | + source: Source::Mine { has_tool: true }, |
| 301 | + }, |
| 302 | + )), |
| 303 | + ), |
| 304 | + ), |
| 305 | + ), |
| 306 | + ) |
| 307 | + // insert <pos> |
| 308 | + .then( |
| 309 | + literal("insert").then( |
| 310 | + argument("pos", BlockPosArgumentType) |
| 311 | + .then(literal("loot").then( |
| 312 | + argument("loot_table", StringArgumentType::SingleWord).executes( |
| 313 | + LootExecutor { |
| 314 | + target: Target::Insert, |
| 315 | + source: Source::Loot, |
| 316 | + }, |
| 317 | + ), |
| 318 | + )) |
| 319 | + .then(literal("kill").then( |
| 320 | + argument("target_entity", EntityArgumentType::Entities).executes( |
| 321 | + LootExecutor { |
| 322 | + target: Target::Insert, |
| 323 | + source: Source::Kill, |
| 324 | + }, |
| 325 | + ), |
| 326 | + )) |
| 327 | + .then( |
| 328 | + literal("mine").then( |
| 329 | + argument("mine_pos", BlockPosArgumentType) |
| 330 | + .executes(LootExecutor { |
| 331 | + target: Target::Insert, |
| 332 | + source: Source::Mine { has_tool: false }, |
| 333 | + }) |
| 334 | + .then(argument("tool", StringArgumentType::SingleWord).executes( |
| 335 | + LootExecutor { |
| 336 | + target: Target::Insert, |
| 337 | + source: Source::Mine { has_tool: true }, |
| 338 | + }, |
| 339 | + )), |
| 340 | + ), |
| 341 | + ), |
| 342 | + ), |
| 343 | + ); |
| 344 | + |
| 345 | + dispatcher.register(builder); |
| 346 | +} |
0 commit comments