use std::collections::HashMap;
use serde_json::Value;
use crate::BlockState;
use crate::utils::{NbtValue, NbtMap};
pub fn parse_block_string(block_string: &str) -> Result<(BlockState, Option<NbtMap>), String> {
let mut parts = block_string.splitn(2, '{');
let block_state_str = parts.next().unwrap().trim();
let nbt_str = parts.next().map(|s| s.trim_end_matches('}'));
let block_state = if block_state_str.contains('[') {
let mut state_parts = block_state_str.splitn(2, '[');
let block_name = state_parts.next().unwrap();
let properties_str = state_parts.next()
.ok_or("Missing properties closing bracket")?
.trim_end_matches(']');
let mut properties = HashMap::new();
for prop in properties_str.split(',') {
let mut kv = prop.split('=');
let key = kv.next().ok_or("Missing property key")?.trim();
let value = kv.next().ok_or("Missing property value")?.trim()
.trim_matches(|c| c == '\'' || c == '"');
properties.insert(key.to_string(), value.to_string());
}
BlockState::new(block_name.to_string()).with_properties(properties)
} else {
BlockState::new(block_state_str.to_string())
};
let nbt_data = if let Some(nbt_str) = nbt_str {
let mut nbt_map = NbtMap::new();
if nbt_str.contains("Items:[") {
let items = parse_items_array(nbt_str)?;
nbt_map.insert("Items".to_string(), NbtValue::List(items));
}
if nbt_str.contains("CustomName:") {
let name = parse_custom_name(nbt_str)?;
nbt_map.insert("CustomName".to_string(), NbtValue::String(name));
}
Some(nbt_map)
} else {
None
};
Ok((block_state, nbt_data))
}
pub fn parse_items_array(nbt_str: &str) -> Result<Vec<NbtValue>, String> {
let items_start = nbt_str.find("Items:[")
.ok_or("Missing Items array")?;
let array_start = items_start + "Items:".len();
let array_str = extract_balanced_substring(&nbt_str[array_start..], '[', ']')
.ok_or("Malformed Items array")?;
let items_content = array_str[1..array_str.len()-1].trim();
let mut items = Vec::new();
for item_str in split_items(items_content) {
let mut item_nbt = NbtMap::new();
let clean_item_str = item_str.trim_matches(|c| c == '{' || c == '}');
for prop in clean_item_str.split(',') {
let prop = prop.trim();
let (key_part, value_part) = prop.split_once(':')
.ok_or_else(|| format!("Invalid property format: '{}'", prop))?;
let key = key_part.trim().trim_matches(|c| c == '"' || c == '\'');
let value = value_part.trim();
match key {
"Count" => {
let count_str = value.trim_matches(|c| c == '"' || c == '\'').trim_end_matches('b');
let count: i8 = count_str.parse()
.map_err(|_| format!("Invalid Count value: {}", count_str))?;
item_nbt.insert("Count".to_string(), NbtValue::Byte(count));
},
"Slot" => {
let slot_str = value.trim_matches(|c| c == '"' || c == '\'').trim_end_matches('b');
let slot: i8 = slot_str.parse()
.map_err(|_| format!("Invalid Slot value: {}", slot_str))?;
item_nbt.insert("Slot".to_string(), NbtValue::Byte(slot));
},
"id" => {
let id = value.trim_matches(|c| c == '"' || c == '\'').to_string();
item_nbt.insert("id".to_string(), NbtValue::String(id));
},
_ => {
}
}
}
if item_nbt.get("Count").is_none() ||
item_nbt.get("Slot").is_none() ||
item_nbt.get("id").is_none() {
return Err("Missing required item properties".to_string());
}
items.push(NbtValue::Compound(item_nbt));
}
Ok(items)
}
fn split_items(items_str: &str) -> Vec<String> {
let mut items = Vec::new();
let mut current_item = String::new();
let mut brace_count = 0;
for c in items_str.chars() {
match c {
'{' => {
brace_count += 1;
current_item.push(c);
}
'}' => {
brace_count -= 1;
current_item.push(c);
if brace_count == 0 {
let item = current_item.trim();
if !item.is_empty() {
items.push(item.to_string());
}
current_item.clear();
}
}
',' if brace_count == 0 => {},
_ => {
if brace_count > 0 {
current_item.push(c);
}
}
}
}
items
}
pub fn parse_custom_name(nbt_str: &str) -> Result<String, String> {
let name_start = nbt_str.find("CustomName:")
.ok_or("No CustomName field found")?
+ "CustomName:".len();
let name_content = nbt_str[name_start..].trim_start();
let name_content = name_content.trim_start_matches('\'');
let name_content = name_content
.split_once(',')
.map(|(name, _)| name)
.unwrap_or_else(|| name_content);
let name_content = name_content.trim_end_matches('\'').trim();
if name_content.starts_with('{') {
match serde_json::from_str::<Value>(name_content) {
Ok(json) => {
json.get("text")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| "Missing or invalid 'text' field in CustomName JSON".to_string())
}
Err(e) => Err(format!("Invalid JSON in CustomName: {}", e))
}
} else {
Ok(name_content.to_string())
}
}
fn extract_balanced_substring(s: &str, open: char, close: char) -> Option<&str> {
let mut depth = 0;
let mut start = None;
for (i, c) in s.chars().enumerate() {
match c {
c if c == open => {
if depth == 0 {
start = Some(i);
}
depth += 1;
}
c if c == close => {
depth -= 1;
if depth == 0 && start.is_some() {
return Some(&s[start.unwrap()..=i]);
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_block_string() {
let block_str = r#"minecraft:barrel[facing=up]{CustomName:'{"text":"Storage"}',Items:[{"Count":"64b","Slot":"0b","id":"minecraft:redstone"}]}"#;
let (block_state, nbt_data) = parse_block_string(block_str).unwrap();
assert_eq!(block_state.get_name(), "minecraft:barrel");
assert_eq!(block_state.get_property("facing"), Some(&"up".to_string()));
let nbt_data = nbt_data.expect("NBT data should be present");
if let Some(NbtValue::List(items)) = nbt_data.get("Items") {
assert_eq!(items.len(), 1);
if let NbtValue::Compound(item) = &items[0] {
assert_eq!(item.get("id"), Some(&NbtValue::String("minecraft:redstone".to_string())));
assert_eq!(item.get("Count"), Some(&NbtValue::Byte(64)));
assert_eq!(item.get("Slot"), Some(&NbtValue::Byte(0)));
} else {
panic!("Expected compound NBT value for item");
}
} else {
panic!("Expected list of items");
}
if let Some(NbtValue::String(name)) = nbt_data.get("CustomName") {
assert_eq!(name, "Storage");
} else {
panic!("Expected CustomName to be a string with value 'Storage'");
}
}
#[test]
fn test_parse_custom_name() {
let test_cases = [
(
r#"CustomName:'{"text":"Test Name"}'"#,
"Test Name",
"Basic JSON case"
),
(
"CustomName:'Plain Text'",
"Plain Text",
"Plain text case"
),
(
r#"CustomName:'{"text":"Test Name"}',OtherField:123"#,
"Test Name",
"JSON with trailing comma"
),
(
"CustomName:'Simple Name',OtherField:123",
"Simple Name",
"Plain text with trailing comma"
)
];
for (input, expected, description) in test_cases {
let result = parse_custom_name(input).unwrap();
assert_eq!(result, expected, "Failed on case: {}", description);
}
}
#[test]
fn test_parse_items_array() {
let nbt_str = r#"Items:[{"Count":"64b","Slot":"0b","id":"minecraft:stone"}]"#;
let items = parse_items_array(nbt_str).unwrap();
assert_eq!(items.len(), 1);
if let NbtValue::Compound(item) = &items[0] {
assert_eq!(item.get("id"), Some(&NbtValue::String("minecraft:stone".to_string())));
assert_eq!(item.get("Count"), Some(&NbtValue::Byte(64)));
assert_eq!(item.get("Slot"), Some(&NbtValue::Byte(0)));
} else {
panic!("Expected compound NBT value");
}
}
}