1use std::borrow::Cow;
2use std::io::{Result, Write};
3
4use steel_macros::{ClientPacket, WriteTo};
5use steel_registry::packets::play::C_COMMANDS;
6use steel_utils::{
7 codec::VarInt,
8 serial::{PrefixedWrite, WriteTo},
9};
10
11#[derive(ClientPacket, WriteTo)]
12#[packet_id(Play = C_COMMANDS)]
13pub struct CCommands {
14 pub nodes: Vec<CommandNode>,
15 #[write(as = VarInt)]
16 pub root_index: i32,
17}
18
19pub enum CommandNode {
20 Root {
21 children: Vec<i32>,
22 },
23 Literal {
24 children: Vec<i32>,
25 redirects_to: Option<i32>,
26 name: Cow<'static, str>,
27 is_executable: bool,
28 is_restricted: bool,
29 },
30 Argument {
31 children: Vec<i32>,
32 redirects_to: Option<i32>,
33 name: Cow<'static, str>,
34 is_executable: bool,
35 is_restricted: bool,
36 parser: ArgumentType,
37 suggestions_type: Option<SuggestionType>,
38 },
39}
40
41impl CommandNode {
42 const FLAG_IS_EXECUTABLE: u8 = 4;
43 const FLAG_HAS_REDIRECT: u8 = 8;
44 const FLAG_HAS_SUGGESTION_TYPE: u8 = 16;
45 const FLAG_IS_RESTRICTED: u8 = 32;
46
47 #[must_use]
48 pub const fn new_root() -> Self {
49 Self::Root {
50 children: Vec::new(),
51 }
52 }
53
54 pub fn new_literal(info: CommandNodeInfo, name: impl Into<Cow<'static, str>>) -> Self {
55 Self::Literal {
56 children: info.children,
57 name: name.into(),
58 is_executable: info.is_executable,
59 is_restricted: info.is_restricted,
60 redirects_to: info.redirects_to,
61 }
62 }
63
64 pub fn new_argument(
65 info: CommandNodeInfo,
66 name: impl Into<Cow<'static, str>>,
67 argument: (ArgumentType, Option<SuggestionType>),
68 ) -> Self {
69 Self::Argument {
70 children: info.children,
71 name: name.into(),
72 is_executable: info.is_executable,
73 is_restricted: info.is_restricted,
74 redirects_to: info.redirects_to,
75 parser: argument.0,
76 suggestions_type: argument.1,
77 }
78 }
79
80 const fn flags(&self) -> u8 {
81 let (mut flags, is_executable, has_redirect, has_suggestions_type, is_restricted) =
82 match self {
83 CommandNode::Root { .. } => (0, false, false, false, false),
84 CommandNode::Literal {
85 is_executable,
86 redirects_to,
87 is_restricted,
88 ..
89 } => (
90 1,
91 *is_executable,
92 redirects_to.is_some(),
93 false,
94 *is_restricted,
95 ),
96 CommandNode::Argument {
97 is_executable,
98 redirects_to: r,
99 suggestions_type,
100 is_restricted,
101 ..
102 } => (
103 2,
104 *is_executable,
105 r.is_some(),
106 suggestions_type.is_some(),
107 *is_restricted,
108 ),
109 };
110
111 if is_executable {
112 flags |= Self::FLAG_IS_EXECUTABLE;
113 }
114 if has_redirect {
115 flags |= Self::FLAG_HAS_REDIRECT;
116 }
117 if has_suggestions_type {
118 flags |= Self::FLAG_HAS_SUGGESTION_TYPE;
119 }
120 if is_restricted {
121 flags |= Self::FLAG_IS_RESTRICTED;
122 }
123 flags
124 }
125
126 pub fn set_children(&mut self, children: Vec<i32>) {
127 match self {
128 CommandNode::Root { children: c } => *c = children,
129 CommandNode::Literal { children: c, .. } => *c = children,
130 CommandNode::Argument { children: c, .. } => *c = children,
131 }
132 }
133
134 fn children(&self) -> &[i32] {
135 match self {
136 CommandNode::Root { children } => children,
137 CommandNode::Literal { children, .. } => children,
138 CommandNode::Argument { children, .. } => children,
139 }
140 }
141
142 const fn redirects_to(&self) -> &Option<i32> {
143 match self {
144 CommandNode::Root { .. } => &None,
145 CommandNode::Literal { redirects_to, .. } => redirects_to,
146 CommandNode::Argument { redirects_to, .. } => redirects_to,
147 }
148 }
149
150 fn name(&self) -> Option<&str> {
151 match self {
152 CommandNode::Root { .. } => None,
153 CommandNode::Literal { name, .. } => Some(name),
154 CommandNode::Argument { name, .. } => Some(name),
155 }
156 }
157}
158
159impl WriteTo for CommandNode {
160 fn write(&self, writer: &mut impl Write) -> Result<()> {
161 writer.write_all(&self.flags().to_be_bytes())?;
162 let children = self.children();
163 VarInt(children.len() as i32).write(writer)?;
164 for child in children {
165 VarInt(*child).write(writer)?;
166 }
167
168 if let Some(redirects_to) = self.redirects_to() {
169 VarInt(*redirects_to).write(writer)?;
170 }
171
172 if let Some(name) = self.name() {
173 name.write_prefixed::<VarInt>(writer)?;
174 }
175
176 if let CommandNode::Argument {
177 parser,
178 suggestions_type,
179 ..
180 } = self
181 {
182 parser.write(writer)?;
183
184 if let Some(suggestions_type) = suggestions_type {
185 suggestions_type.as_str().write_prefixed::<VarInt>(writer)?;
186 }
187 }
188
189 Ok(())
190 }
191}
192
193pub struct CommandNodeInfo {
194 children: Vec<i32>,
195 is_executable: bool,
196 is_restricted: bool,
197 redirects_to: Option<i32>,
198}
199
200impl CommandNodeInfo {
201 #[must_use]
202 pub const fn new(children: Vec<i32>) -> Self {
203 Self {
204 children,
205 is_executable: false,
206 is_restricted: false,
207 redirects_to: None,
208 }
209 }
210
211 #[must_use]
212 pub const fn new_executable() -> Self {
213 Self {
214 children: Vec::new(),
215 is_executable: true,
216 is_restricted: false,
217 redirects_to: None,
218 }
219 }
220
221 #[must_use]
222 pub const fn new_redirect(redirects_to: i32) -> Self {
223 Self {
224 children: Vec::new(),
225 is_executable: false,
226 is_restricted: false,
227 redirects_to: Some(redirects_to),
228 }
229 }
230
231 #[must_use]
233 pub const fn executable(mut self) -> Self {
234 self.is_executable = true;
235 self
236 }
237
238 #[must_use]
240 pub const fn restricted(mut self) -> Self {
241 self.is_restricted = true;
242 self
243 }
244
245 #[must_use]
247 pub const fn redirect(mut self, redirects_to: i32) -> Self {
248 self.redirects_to = Some(redirects_to);
249 self
250 }
251
252 #[must_use]
253 pub fn chain(mut self, mut other: Self) -> Self {
254 self.children.append(&mut other.children);
255 self.is_executable |= other.is_executable;
256 self.is_restricted |= other.is_restricted;
257 self
258 }
259}
260
261pub enum ArgumentType {
262 Bool,
263 Float {
264 min: Option<f32>,
265 max: Option<f32>,
266 },
267 Double {
268 min: Option<f64>,
269 max: Option<f64>,
270 },
271 Integer {
272 min: Option<i32>,
273 max: Option<i32>,
274 },
275 Long {
276 min: Option<i64>,
277 max: Option<i64>,
278 },
279 String {
280 behavior: ArgumentStringTypeBehavior,
281 },
282 Entity {
283 flags: u8,
284 },
285 GameProfile,
286 BlockPos,
287 ColumnPos,
288 Vec3,
289 Vec2,
290 BlockState,
291 BlockPredicate,
292 ItemStack,
293 ItemPredicate,
294 Color,
295 HexColor,
296 Component,
297 Style,
298 Message,
299 Nbt,
300 NbtTag,
301 NbtPath,
302 Objective,
303 ObjectiveCriteria,
304 Operation,
305 Particle,
306 Angle,
307 Rotation,
308 ScoreboardSlot,
309 ScoreHolder {
310 flags: u8,
311 },
312 Swizzle,
313 Team,
314 ItemSlot,
315 ItemSlots,
316 ResourceLocation,
317 Function,
318 EntityAnchor,
319 IntRange,
320 FloatRange,
321 Dimension,
322 Gamemode,
323 Time {
324 min: i32,
325 },
326 ResourceOrTag {
327 identifier: &'static str,
328 },
329 ResourceOrTagKey {
330 identifier: &'static str,
331 },
332 Resource {
333 identifier: &'static str,
334 },
335 ResourceKey {
336 identifier: &'static str,
337 },
338 ResourceSelector {
339 identifier: &'static str,
340 },
341 TemplateMirror,
342 TemplateRotation,
343 Heightmap,
344 LootTable,
345 LootPredicate,
346 LootModifier,
347 Dialog,
348 Uuid,
349}
350
351#[derive(Debug, Clone, Copy)]
352pub enum ArgumentStringTypeBehavior {
353 SingleWord,
354 QuotablePhrase,
355 GreedyPhrase,
356}
357
358impl ArgumentType {
359 const fn discriminant(&self) -> i32 {
360 match self {
361 Self::Bool => 0,
362 Self::Float { .. } => 1,
363 Self::Double { .. } => 2,
364 Self::Integer { .. } => 3,
365 Self::Long { .. } => 4,
366 Self::String { .. } => 5,
367 Self::Entity { .. } => 6,
368 Self::GameProfile => 7,
369 Self::BlockPos => 8,
370 Self::ColumnPos => 9,
371 Self::Vec3 => 10,
372 Self::Vec2 => 11,
373 Self::BlockState => 12,
374 Self::BlockPredicate => 13,
375 Self::ItemStack => 14,
376 Self::ItemPredicate => 15,
377 Self::Color => 16,
378 Self::HexColor => 17,
379 Self::Component => 18,
380 Self::Style => 19,
381 Self::Message => 20,
382 Self::Nbt => 21,
383 Self::NbtTag => 22,
384 Self::NbtPath => 23,
385 Self::Objective => 24,
386 Self::ObjectiveCriteria => 25,
387 Self::Operation => 26,
388 Self::Particle => 27,
389 Self::Angle => 28,
390 Self::Rotation => 29,
391 Self::ScoreboardSlot => 30,
392 Self::ScoreHolder { .. } => 31,
393 Self::Swizzle => 32,
394 Self::Team => 33,
395 Self::ItemSlot => 34,
396 Self::ItemSlots => 35,
397 Self::ResourceLocation => 36,
398 Self::Function => 37,
399 Self::EntityAnchor => 38,
400 Self::IntRange => 39,
401 Self::FloatRange => 40,
402 Self::Dimension => 41,
403 Self::Gamemode => 42,
404 Self::Time { .. } => 43,
405 Self::ResourceOrTag { .. } => 44,
406 Self::ResourceOrTagKey { .. } => 45,
407 Self::Resource { .. } => 46,
408 Self::ResourceKey { .. } => 47,
409 Self::ResourceSelector { .. } => 48,
410 Self::TemplateMirror => 49,
411 Self::TemplateRotation => 50,
412 Self::Heightmap => 51,
413 Self::LootTable => 52,
414 Self::LootPredicate => 53,
415 Self::LootModifier => 54,
416 Self::Dialog => 55,
417 Self::Uuid => 56,
418 }
419 }
420}
421
422impl WriteTo for ArgumentType {
423 fn write(&self, writer: &mut impl Write) -> Result<()> {
424 VarInt(self.discriminant()).write(writer)?;
425
426 match self {
427 Self::Float { min, max } => Self::write_min_max(*min, *max, writer),
428 Self::Double { min, max } => Self::write_min_max(*min, *max, writer),
429 Self::Integer { min, max } => Self::write_min_max(*min, *max, writer),
430 Self::Long { min, max } => Self::write_min_max(*min, *max, writer),
431 Self::String { behavior } => {
432 let i = match behavior {
433 ArgumentStringTypeBehavior::SingleWord => 0,
434 ArgumentStringTypeBehavior::QuotablePhrase => 1,
435 ArgumentStringTypeBehavior::GreedyPhrase => 2,
436 };
437 VarInt(i).write(writer)
438 }
439 Self::Entity { flags } => flags.write(writer),
440 Self::ScoreHolder { flags } => flags.write(writer),
441 Self::Time { min } => min.write(writer),
442 Self::ResourceOrTag { identifier } => identifier.write_prefixed::<VarInt>(writer),
443 Self::ResourceOrTagKey { identifier } => identifier.write_prefixed::<VarInt>(writer),
444 Self::Resource { identifier } => identifier.write_prefixed::<VarInt>(writer),
445 Self::ResourceKey { identifier } => identifier.write_prefixed::<VarInt>(writer),
446 Self::ResourceSelector { identifier } => identifier.write_prefixed::<VarInt>(writer),
447 _ => Ok(()),
448 }
449 }
450}
451
452impl ArgumentType {
453 fn write_min_max<T: WriteTo>(
454 min: Option<T>,
455 max: Option<T>,
456 writer: &mut impl Write,
457 ) -> Result<()> {
458 (u8::from(min.is_some()) + u8::from(max.is_some()) + u8::from(max.is_some()))
463 .write(writer)?;
464
465 if let Some(min) = min {
466 min.write(writer)?;
467 }
468 if let Some(max) = max {
469 max.write(writer)?;
470 }
471
472 Ok(())
473 }
474}
475
476pub enum SuggestionType {
477 AskServer,
478 AllRecipes,
479 AvailableSounds,
480 SummonableEntities,
481}
482
483impl SuggestionType {
484 const fn as_str(&self) -> &str {
485 match self {
486 SuggestionType::AskServer => "minecraft:ask_server",
487 SuggestionType::AllRecipes => "minecraft:all_recipes",
488 SuggestionType::AvailableSounds => "minecraft:available_sounds",
489 SuggestionType::SummonableEntities => "minecraft:summonable_entities",
490 }
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use steel_utils::serial::WriteTo;
497
498 use super::{ArgumentType, CommandNode, CommandNodeInfo};
499
500 #[test]
501 fn restricted_nodes_set_the_26_2_protocol_flag() {
502 let node = CommandNode::new_literal(CommandNodeInfo::new(Vec::new()).restricted(), "op");
503 let mut encoded = Vec::new();
504
505 assert!(node.write(&mut encoded).is_ok());
506 assert_eq!(encoded.first().copied(), Some(1 | 32));
507 }
508
509 #[test]
510 fn command_node_flags_can_be_combined() {
511 let info = CommandNodeInfo::new(Vec::new())
512 .executable()
513 .restricted()
514 .redirect(0);
515 let node = CommandNode::new_literal(info, "alias");
516 let mut encoded = Vec::new();
517
518 assert!(node.write(&mut encoded).is_ok());
519 assert_eq!(encoded.first().copied(), Some(1 | 4 | 8 | 32));
520 }
521
522 #[test]
523 fn command_argument_tail_uses_the_26_2_registry_ids() {
524 for (argument, expected) in [
525 (ArgumentType::TemplateMirror, 49),
526 (ArgumentType::TemplateRotation, 50),
527 (ArgumentType::Heightmap, 51),
528 (ArgumentType::LootTable, 52),
529 (ArgumentType::LootPredicate, 53),
530 (ArgumentType::LootModifier, 54),
531 (ArgumentType::Dialog, 55),
532 (ArgumentType::Uuid, 56),
533 ] {
534 let mut encoded = Vec::new();
535 assert!(argument.write(&mut encoded).is_ok());
536 assert_eq!(encoded, [expected]);
537 }
538 }
539
540 #[test]
541 fn resource_selector_writes_its_registry_key() {
542 let mut encoded = Vec::new();
543 let argument = ArgumentType::ResourceSelector {
544 identifier: "minecraft:test_instance",
545 };
546
547 assert!(argument.write(&mut encoded).is_ok());
548 assert_eq!(&encoded[..2], [48, 23]);
549 assert_eq!(&encoded[2..], b"minecraft:test_instance");
550 }
551}