Skip to content

Commit 06fa1db

Browse files
committed
feat(command): add loot command
1 parent bb5ae5a commit 06fa1db

2 files changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
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+
}

pumpkin/src/command/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod item;
3434
mod kick;
3535
mod kill;
3636
mod list;
37+
mod loot;
3738
mod me;
3839
mod msg;
3940
mod op;
@@ -180,6 +181,7 @@ pub async fn default_dispatcher(
180181
op::register(&mut dispatcher, registry);
181182
random::register(&mut dispatcher, registry);
182183
list::register(&mut dispatcher, registry);
184+
loot::register(&mut dispatcher, registry);
183185
seed::register(&mut dispatcher, registry);
184186
saveall::register(&mut dispatcher, registry);
185187
saveoff::register(&mut dispatcher, registry);

0 commit comments

Comments
 (0)