Skip to content

Commit bb5ae5a

Browse files
committed
feat(command): add recipe command
1 parent 5692c65 commit bb5ae5a

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

pumpkin/src/command/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod plugin;
4545
mod plugins;
4646
mod pumpkin;
4747
mod random;
48+
mod recipe;
4849
mod ride;
4950
mod rotate;
5051
mod saveall;
@@ -173,6 +174,7 @@ pub async fn default_dispatcher(
173174
execute::register(&mut dispatcher, registry);
174175
forceload::register(&mut dispatcher, registry);
175176
ride::register(&mut dispatcher, registry);
177+
recipe::register(&mut dispatcher, registry);
176178
help::register(&mut dispatcher, registry);
177179
kill::register(&mut dispatcher, registry);
178180
op::register(&mut dispatcher, registry);
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
2+
use crate::command::argument_types::core::string::StringArgumentType;
3+
use crate::command::argument_types::entity::EntityArgumentType;
4+
use crate::command::context::command_context::CommandContext;
5+
use crate::command::errors::error_types::CommandErrorType;
6+
use crate::command::node::dispatcher::CommandDispatcher;
7+
use crate::command::node::{CommandExecutor, CommandExecutorResult};
8+
use crate::command::suggestion::provider::SuggestionProvider;
9+
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder};
10+
use crate::entity::EntityBase;
11+
use pumpkin_data::translation;
12+
use pumpkin_protocol::codec::recipe::DynamicRecipe;
13+
use pumpkin_protocol::java::client::play::CRecipeBookAdd;
14+
use pumpkin_util::PermissionLvl;
15+
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
16+
use pumpkin_util::text::TextComponent;
17+
use std::future::Future;
18+
use std::pin::Pin;
19+
20+
const DESCRIPTION: &str = "Gives or takes player recipes.";
21+
const PERMISSION: &str = "minecraft:command.recipe";
22+
23+
static ERROR_RECIPE_NOT_FOUND: CommandErrorType<1> =
24+
CommandErrorType::new(translation::java::RECIPE_NOTFOUND, "Unknown recipe: %s");
25+
26+
fn get_recipe_id(recipe: &DynamicRecipe) -> String {
27+
match recipe {
28+
DynamicRecipe::Crafting(crafting) => match crafting {
29+
pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shaped { result, .. }
30+
| pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shapeless { result, .. } => {
31+
result.item_id.clone()
32+
}
33+
},
34+
DynamicRecipe::Cooking(cooking) => match cooking {
35+
pumpkin_protocol::codec::recipe::OwnedCookingRecipeType::Smelting(r)
36+
| pumpkin_protocol::codec::recipe::OwnedCookingRecipeType::Blasting(r)
37+
| pumpkin_protocol::codec::recipe::OwnedCookingRecipeType::Smoking(r)
38+
| pumpkin_protocol::codec::recipe::OwnedCookingRecipeType::CampfireCooking(r) => {
39+
r.recipe_id.clone()
40+
}
41+
},
42+
}
43+
}
44+
45+
struct RecipeSuggestionProvider;
46+
47+
impl SuggestionProvider for RecipeSuggestionProvider {
48+
fn suggest(
49+
&self,
50+
context: &CommandContext,
51+
mut builder: SuggestionsBuilder,
52+
) -> Pin<Box<dyn Future<Output = Suggestions> + Send>> {
53+
let server = context.source.server.clone();
54+
55+
Box::pin(async move {
56+
builder = builder.suggest("*");
57+
if let Some(server) = server {
58+
let recipes = server.recipe_manager.get_dynamic_recipes_internal().await;
59+
for recipe in recipes {
60+
let id = get_recipe_id(&recipe);
61+
builder = builder.suggest(id);
62+
}
63+
}
64+
builder.build()
65+
})
66+
}
67+
}
68+
69+
struct RecipeGiveExecutor;
70+
71+
impl CommandExecutor for RecipeGiveExecutor {
72+
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
73+
Box::pin(async move {
74+
let targets = EntityArgumentType::get_players(context, "targets").await?;
75+
let recipe_str = StringArgumentType::get(context, "recipe")?;
76+
77+
let server = context.source.server.as_ref().ok_or_else(|| {
78+
ERROR_RECIPE_NOT_FOUND
79+
.create_without_context(TextComponent::text(recipe_str.to_string()))
80+
})?;
81+
82+
let all_recipes = server.recipe_manager.get_dynamic_recipes_internal().await;
83+
84+
let is_all = recipe_str == "*";
85+
let matching_recipes = if is_all {
86+
all_recipes.clone()
87+
} else {
88+
all_recipes
89+
.iter()
90+
.filter(|r| {
91+
let id = get_recipe_id(r);
92+
id == recipe_str
93+
|| id.strip_prefix("minecraft:").unwrap_or(&id) == recipe_str
94+
})
95+
.cloned()
96+
.collect::<Vec<_>>()
97+
};
98+
99+
if !is_all && matching_recipes.is_empty() {
100+
return Err(ERROR_RECIPE_NOT_FOUND
101+
.create_without_context(TextComponent::text(recipe_str.to_string())));
102+
}
103+
104+
let recipe_count = matching_recipes.len();
105+
106+
for player in &targets {
107+
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() {
108+
java_client
109+
.send_packet_now(&CRecipeBookAdd::new(false, &matching_recipes))
110+
.await;
111+
}
112+
}
113+
114+
let recipe_count_str = recipe_count.to_string();
115+
if targets.len() == 1 {
116+
let msg = TextComponent::translate_cross(
117+
translation::java::COMMANDS_RECIPE_GIVE_SUCCESS_SINGLE,
118+
translation::java::COMMANDS_RECIPE_GIVE_SUCCESS_SINGLE,
119+
[
120+
TextComponent::text(recipe_count_str),
121+
targets[0].get_display_name().await,
122+
],
123+
);
124+
context.source.send_feedback(msg, true).await;
125+
} else {
126+
let msg = TextComponent::translate_cross(
127+
translation::java::COMMANDS_RECIPE_GIVE_SUCCESS_MULTIPLE,
128+
translation::java::COMMANDS_RECIPE_GIVE_SUCCESS_MULTIPLE,
129+
[
130+
TextComponent::text(recipe_count_str),
131+
TextComponent::text(targets.len().to_string()),
132+
],
133+
);
134+
context.source.send_feedback(msg, true).await;
135+
}
136+
137+
Ok((targets.len() * recipe_count) as i32)
138+
})
139+
}
140+
}
141+
142+
struct RecipeTakeExecutor;
143+
144+
impl CommandExecutor for RecipeTakeExecutor {
145+
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
146+
Box::pin(async move {
147+
let targets = EntityArgumentType::get_players(context, "targets").await?;
148+
let recipe_str = StringArgumentType::get(context, "recipe")?;
149+
150+
let server = context.source.server.as_ref().ok_or_else(|| {
151+
ERROR_RECIPE_NOT_FOUND
152+
.create_without_context(TextComponent::text(recipe_str.to_string()))
153+
})?;
154+
155+
let all_recipes = server.recipe_manager.get_dynamic_recipes_internal().await;
156+
157+
let is_all = recipe_str == "*";
158+
159+
let mut matched = false;
160+
let remaining_recipes = if is_all {
161+
matched = true;
162+
Vec::new()
163+
} else {
164+
all_recipes
165+
.iter()
166+
.filter(|r| {
167+
let id = get_recipe_id(r);
168+
let is_match = id == recipe_str
169+
|| id.strip_prefix("minecraft:").unwrap_or(&id) == recipe_str;
170+
if is_match {
171+
matched = true;
172+
}
173+
!is_match
174+
})
175+
.cloned()
176+
.collect::<Vec<_>>()
177+
};
178+
179+
if !matched {
180+
return Err(ERROR_RECIPE_NOT_FOUND
181+
.create_without_context(TextComponent::text(recipe_str.to_string())));
182+
}
183+
184+
let taken_count = if is_all {
185+
all_recipes.len()
186+
} else {
187+
all_recipes.len() - remaining_recipes.len()
188+
};
189+
190+
for player in &targets {
191+
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() {
192+
java_client
193+
.send_packet_now(&CRecipeBookAdd::new(true, &remaining_recipes))
194+
.await;
195+
}
196+
}
197+
198+
let taken_count_str = taken_count.to_string();
199+
if targets.len() == 1 {
200+
let msg = TextComponent::translate_cross(
201+
translation::java::COMMANDS_RECIPE_TAKE_SUCCESS_SINGLE,
202+
translation::java::COMMANDS_RECIPE_TAKE_SUCCESS_SINGLE,
203+
[
204+
TextComponent::text(taken_count_str),
205+
targets[0].get_display_name().await,
206+
],
207+
);
208+
context.source.send_feedback(msg, true).await;
209+
} else {
210+
let msg = TextComponent::translate_cross(
211+
translation::java::COMMANDS_RECIPE_TAKE_SUCCESS_MULTIPLE,
212+
translation::java::COMMANDS_RECIPE_TAKE_SUCCESS_MULTIPLE,
213+
[
214+
TextComponent::text(taken_count_str),
215+
TextComponent::text(targets.len().to_string()),
216+
],
217+
);
218+
context.source.send_feedback(msg, true).await;
219+
}
220+
221+
Ok((targets.len() * taken_count) as i32)
222+
})
223+
}
224+
}
225+
226+
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
227+
registry.register_permission_or_panic(Permission::new(
228+
PERMISSION,
229+
DESCRIPTION,
230+
PermissionDefault::Op(PermissionLvl::Two),
231+
));
232+
233+
let builder = command("recipe", DESCRIPTION)
234+
.requires(PERMISSION)
235+
.then(
236+
literal("give").then(
237+
argument("targets", EntityArgumentType::Players).then(
238+
argument("recipe", StringArgumentType::SingleWord)
239+
.suggests(RecipeSuggestionProvider)
240+
.executes(RecipeGiveExecutor),
241+
),
242+
),
243+
)
244+
.then(
245+
literal("take").then(
246+
argument("targets", EntityArgumentType::Players).then(
247+
argument("recipe", StringArgumentType::SingleWord)
248+
.suggests(RecipeSuggestionProvider)
249+
.executes(RecipeTakeExecutor),
250+
),
251+
),
252+
);
253+
254+
dispatcher.register(builder);
255+
}

0 commit comments

Comments
 (0)