1use std::path::PathBuf;
2
3use clap::{ArgGroup, Parser, ValueEnum};
4use valence_nbt::Value;
5
6#[derive(Parser, Debug)]
8#[command(group(ArgGroup::new("mode").args(["all", "items"]).required(true)))]
9pub struct CliArgs {
10 #[arg(short, long, value_name = "PATH")]
11 pub world_path: PathBuf,
12
13 #[arg(long, group = "mode")]
15 pub all: bool,
16
17 #[arg(
19 short,
20 long = "item",
21 value_name = "ITEM",
22 group = "mode",
23 num_args = 1..,
24 long_help = "Specify items to count, each in the form: ITEM_ID{nbt}\n\nExamples:\n\n--item minecraft:diamond\n--item minecraft:shulker_box{components:{\"minecraft:item_name\":\"Portable Chest\"}}"
25 )]
26 pub items: Vec<String>,
27
28 #[arg(short, long, value_enum, default_value_t = ViewMode::ById)]
30 pub view: ViewMode,
31
32 #[arg(long)]
34 pub show_nbt: bool,
35
36 #[arg(long)]
38 pub per_source_summary: bool,
39
40 #[arg(long)]
42 pub per_dimension_summary: bool,
43
44 #[arg(long)]
46 pub per_data_type_summary: bool,
47
48 #[arg(long)]
50 pub verbose: bool,
51
52 #[arg(short, long, value_enum, default_value_t = OutputFormat::Table)]
54 pub format: OutputFormat,
55}
56
57#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
59pub enum ViewMode {
60 Detailed,
62
63 ById,
65
66 ByNbt,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
72pub enum OutputFormat {
73 Table,
74 Json,
75 PrettyJson,
76}
77
78impl OutputFormat {
79 pub fn is_json(&self) -> bool {
80 matches!(self, OutputFormat::Json | OutputFormat::PrettyJson)
81 }
82}
83
84#[derive(Debug)]
86pub struct ItemFilter {
87 pub id: Option<String>,
88 pub required_nbt: Option<Value>,
89}
90
91pub fn parse_item_args(raw_items: &[String]) -> Vec<ItemFilter> {
94 raw_items
95 .iter()
96 .map(|entry| {
97 let mut id_str = entry.as_str();
98 let mut nbt_query = None;
99
100 if let Some(start) = entry.find('{')
101 && let Some(end) = entry.rfind('}')
102 {
103 id_str = &entry[..start];
104 let nbt_str = &entry[start..=end];
105 if !nbt_str.is_empty() {
106 match valence_nbt::snbt::from_snbt_str(nbt_str) {
107 Ok(parsed) => nbt_query = Some(parsed),
108 Err(e) => eprintln!("Failed to parse SNBT '{nbt_str}': {e}"),
109 }
110 }
111 }
112
113 let id = if id_str.is_empty() {
114 None
115 } else if id_str.contains(':') {
116 Some(id_str.to_string())
117 } else {
118 Some(format!("minecraft:{id_str}"))
119 };
120
121 ItemFilter {
122 id,
123 required_nbt: nbt_query,
124 }
125 })
126 .collect()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use valence_nbt::compound;
133
134 #[test]
135 fn test_parse_item_args_simple_id() {
136 let args = vec!["diamond".to_string()];
137 let filters = parse_item_args(&args);
138 assert_eq!(filters.len(), 1);
139 assert_eq!(filters[0].id, Some("minecraft:diamond".to_string()));
140 assert!(filters[0].required_nbt.is_none());
141 }
142
143 #[test]
144 fn test_parse_item_args_namespaced_id() {
145 let args = vec!["custom:item".to_string()];
146 let filters = parse_item_args(&args);
147 assert_eq!(filters.len(), 1);
148 assert_eq!(filters[0].id, Some("custom:item".to_string()));
149 assert!(filters[0].required_nbt.is_none());
150 }
151
152 #[test]
153 fn test_parse_item_args_id_with_simple_nbt() {
154 let args = vec!["stone{a:1b}".to_string()];
155 let filters = parse_item_args(&args);
156 assert_eq!(filters.len(), 1);
157 assert_eq!(filters[0].id, Some("minecraft:stone".to_string()));
158 assert_eq!(
159 filters[0].required_nbt,
160 Some(compound! { "a" => 1i8 }.into())
161 );
162 }
163
164 #[test]
165 fn test_parse_item_args_id_with_complex_nbt() {
166 let args = vec!["shulker_box{components:{\"minecraft:container\":[{slot:0b,item:{id:\"minecraft:diamond\",count:1b}}]}}".to_string()];
167 let filters = parse_item_args(&args);
168 assert_eq!(filters.len(), 1);
169 assert_eq!(filters[0].id, Some("minecraft:shulker_box".to_string()));
170 let expected_nbt = valence_nbt::snbt::from_snbt_str("{components:{\"minecraft:container\":[{slot:0b,item:{id:\"minecraft:diamond\",count:1b}}]}}").unwrap();
171 assert_eq!(filters[0].required_nbt, Some(expected_nbt));
172 }
173
174 #[test]
175 fn test_parse_item_args_nbt_only() {
176 let args = vec!["{components:{\"minecraft:custom_name\":\"Special\"}}".to_string()];
177 let filters = parse_item_args(&args);
178 assert_eq!(filters.len(), 1);
179 assert!(filters[0].id.is_none());
180 let expected_nbt = valence_nbt::snbt::from_snbt_str(
181 "{components:{\"minecraft:custom_name\":\"Special\"}}",
182 )
183 .unwrap();
184 assert_eq!(filters[0].required_nbt, Some(expected_nbt));
185 }
186
187 #[test]
188 fn test_parse_item_args_invalid_nbt_string() {
189 let args = vec!["iron_ingot{invalid_nbt:}".to_string()];
191 let filters = parse_item_args(&args);
192 assert_eq!(filters.len(), 1);
193 assert_eq!(filters[0].id, Some("minecraft:iron_ingot".to_string()));
194 assert!(
195 filters[0].required_nbt.is_none(),
196 "NBT should be None for invalid SNBT"
197 );
198 }
199
200 #[test]
201 fn test_parse_item_args_multiple_items() {
202 let args = vec![
203 "diamond".to_string(),
204 "gold_ingot{components:{\"minecraft:custom_data\":{foo:\"bar\"}}}".to_string(),
205 ];
206 let filters = parse_item_args(&args);
207 assert_eq!(filters.len(), 2);
208 assert_eq!(filters[0].id, Some("minecraft:diamond".to_string()));
209 assert!(filters[0].required_nbt.is_none());
210 assert_eq!(filters[1].id, Some("minecraft:gold_ingot".to_string()));
211 let expected_nbt_for_gold = valence_nbt::snbt::from_snbt_str(
212 "{components:{\"minecraft:custom_data\":{foo:\"bar\"}}}",
213 )
214 .unwrap();
215 assert_eq!(filters[1].required_nbt, Some(expected_nbt_for_gold));
216 }
217}