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 SwingAnimation,
349 Uuid,
350}
351
352#[derive(Debug, Clone, Copy)]
353pub enum ArgumentStringTypeBehavior {
354 SingleWord,
355 QuotablePhrase,
356 GreedyPhrase,
357}
358
359impl ArgumentType {
360 const fn discriminant(&self) -> i32 {
361 match self {
362 Self::Bool => 0,
363 Self::Float { .. } => 1,
364 Self::Double { .. } => 2,
365 Self::Integer { .. } => 3,
366 Self::Long { .. } => 4,
367 Self::String { .. } => 5,
368 Self::Entity { .. } => 6,
369 Self::GameProfile => 7,
370 Self::BlockPos => 8,
371 Self::ColumnPos => 9,
372 Self::Vec3 => 10,
373 Self::Vec2 => 11,
374 Self::BlockState => 12,
375 Self::BlockPredicate => 13,
376 Self::ItemStack => 14,
377 Self::ItemPredicate => 15,
378 Self::Color => 16,
379 Self::HexColor => 17,
380 Self::Component => 18,
381 Self::Style => 19,
382 Self::Message => 20,
383 Self::Nbt => 21,
384 Self::NbtTag => 22,
385 Self::NbtPath => 23,
386 Self::Objective => 24,
387 Self::ObjectiveCriteria => 25,
388 Self::Operation => 26,
389 Self::Particle => 27,
390 Self::Angle => 28,
391 Self::Rotation => 29,
392 Self::ScoreboardSlot => 30,
393 Self::ScoreHolder { .. } => 31,
394 Self::Swizzle => 32,
395 Self::Team => 33,
396 Self::ItemSlot => 34,
397 Self::ItemSlots => 35,
398 Self::ResourceLocation => 36,
399 Self::Function => 37,
400 Self::EntityAnchor => 38,
401 Self::IntRange => 39,
402 Self::FloatRange => 40,
403 Self::Dimension => 41,
404 Self::Gamemode => 42,
405 Self::Time { .. } => 43,
406 Self::ResourceOrTag { .. } => 44,
407 Self::ResourceOrTagKey { .. } => 45,
408 Self::Resource { .. } => 46,
409 Self::ResourceKey { .. } => 47,
410 Self::ResourceSelector { .. } => 48,
411 Self::TemplateMirror => 49,
412 Self::TemplateRotation => 50,
413 Self::Heightmap => 51,
414 Self::LootTable => 52,
415 Self::LootPredicate => 53,
416 Self::LootModifier => 54,
417 Self::Dialog => 58,
418 Self::SwingAnimation => 60,
419 Self::Uuid => 61,
420 }
421 }
422}
423
424impl WriteTo for ArgumentType {
425 fn write(&self, writer: &mut impl Write) -> Result<()> {
426 VarInt(self.discriminant()).write(writer)?;
427
428 match self {
429 Self::Float { min, max } => Self::write_min_max(*min, *max, writer),
430 Self::Double { min, max } => Self::write_min_max(*min, *max, writer),
431 Self::Integer { min, max } => Self::write_min_max(*min, *max, writer),
432 Self::Long { min, max } => Self::write_min_max(*min, *max, writer),
433 Self::String { behavior } => {
434 let i = match behavior {
435 ArgumentStringTypeBehavior::SingleWord => 0,
436 ArgumentStringTypeBehavior::QuotablePhrase => 1,
437 ArgumentStringTypeBehavior::GreedyPhrase => 2,
438 };
439 VarInt(i).write(writer)
440 }
441 Self::Entity { flags } => flags.write(writer),
442 Self::ScoreHolder { flags } => flags.write(writer),
443 Self::Time { min } => min.write(writer),
444 Self::ResourceOrTag { identifier } => identifier.write_prefixed::<VarInt>(writer),
445 Self::ResourceOrTagKey { identifier } => identifier.write_prefixed::<VarInt>(writer),
446 Self::Resource { identifier } => identifier.write_prefixed::<VarInt>(writer),
447 Self::ResourceKey { identifier } => identifier.write_prefixed::<VarInt>(writer),
448 Self::ResourceSelector { identifier } => identifier.write_prefixed::<VarInt>(writer),
449 _ => Ok(()),
450 }
451 }
452}
453
454impl ArgumentType {
455 fn write_min_max<T: WriteTo>(
456 min: Option<T>,
457 max: Option<T>,
458 writer: &mut impl Write,
459 ) -> Result<()> {
460 (u8::from(min.is_some()) + u8::from(max.is_some()) + u8::from(max.is_some()))
465 .write(writer)?;
466
467 if let Some(min) = min {
468 min.write(writer)?;
469 }
470 if let Some(max) = max {
471 max.write(writer)?;
472 }
473
474 Ok(())
475 }
476}
477
478pub enum SuggestionType {
479 AskServer,
480 AllRecipes,
481 AvailableSounds,
482 SummonableEntities,
483}
484
485impl SuggestionType {
486 const fn as_str(&self) -> &str {
487 match self {
488 SuggestionType::AskServer => "minecraft:ask_server",
489 SuggestionType::AllRecipes => "minecraft:all_recipes",
490 SuggestionType::AvailableSounds => "minecraft:available_sounds",
491 SuggestionType::SummonableEntities => "minecraft:summonable_entities",
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use steel_utils::serial::WriteTo;
499
500 use super::{ArgumentType, CommandNode, CommandNodeInfo};
501
502 #[test]
503 fn restricted_nodes_set_the_26_2_protocol_flag() {
504 let node = CommandNode::new_literal(CommandNodeInfo::new(Vec::new()).restricted(), "op");
505 let mut encoded = Vec::new();
506
507 assert!(node.write(&mut encoded).is_ok());
508 assert_eq!(encoded.first().copied(), Some(1 | 32));
509 }
510
511 #[test]
512 fn command_node_flags_can_be_combined() {
513 let info = CommandNodeInfo::new(Vec::new())
514 .executable()
515 .restricted()
516 .redirect(0);
517 let node = CommandNode::new_literal(info, "alias");
518 let mut encoded = Vec::new();
519
520 assert!(node.write(&mut encoded).is_ok());
521 assert_eq!(encoded.first().copied(), Some(1 | 4 | 8 | 32));
522 }
523
524 #[test]
525 fn command_argument_tail_uses_the_26_3_registry_ids() {
526 for (argument, expected) in [
527 (ArgumentType::TemplateMirror, 49),
528 (ArgumentType::TemplateRotation, 50),
529 (ArgumentType::Heightmap, 51),
530 (ArgumentType::LootTable, 52),
531 (ArgumentType::LootPredicate, 53),
532 (ArgumentType::LootModifier, 54),
533 (ArgumentType::Dialog, 58),
534 (ArgumentType::SwingAnimation, 60),
535 (ArgumentType::Uuid, 61),
536 ] {
537 let mut encoded = Vec::new();
538 assert!(argument.write(&mut encoded).is_ok());
539 assert_eq!(encoded, [expected]);
540 }
541 }
542
543 #[test]
544 fn resource_selector_writes_its_registry_key() {
545 let mut encoded = Vec::new();
546 let argument = ArgumentType::ResourceSelector {
547 identifier: "minecraft:test_instance",
548 };
549
550 assert!(argument.write(&mut encoded).is_ok());
551 assert_eq!(&encoded[..2], [48, 23]);
552 assert_eq!(&encoded[2..], b"minecraft:test_instance");
553 }
554}