use crate::pack_format::{PackFormat, SUPPORTED_PACK_FORMAT};
use crate::stdlib::{EventType, StdLib};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SourceLocation {
pub file: PathBuf,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum GeneratedCommandKind {
UserCommand,
StdLib,
RuntimeSetup,
ControlFlow,
JsonGenerated,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GeneratedCommand {
pub text: String,
pub source: Option<SourceLocation>,
pub kind: GeneratedCommandKind,
}
impl GeneratedCommand {
pub fn new(text: String, source: Option<SourceLocation>, kind: GeneratedCommandKind) -> Self {
Self { text, source, kind }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMapEntry {
pub generated_path: String,
pub generated_line: usize,
pub command: String,
pub source: Option<SourceLocation>,
pub kind: GeneratedCommandKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMap {
pub version: u8,
pub entries: Vec<SourceMapEntry>,
}
pub struct DataPack {
pub namespace: String,
pub description: String,
pub output_dir: PathBuf,
pub functions: HashMap<String, Vec<String>>,
pub command_metadata: HashMap<String, HashMap<usize, GeneratedCommand>>,
pub tags: HashMap<String, Vec<String>>,
pub advancements: HashMap<String, String>,
pub loot_tables: HashMap<String, String>,
pub recipes: HashMap<String, String>,
pub predicates: HashMap<String, String>,
pub item_modifiers: HashMap<String, String>,
pub json_resources: HashMap<String, String>,
pub pack_format: PackFormat,
pub stdlib: StdLib,
pub used_objectives: HashSet<String>,
}
impl DataPack {
pub fn new(namespace: String, output_dir: PathBuf) -> Self {
Self {
namespace,
description: "Generated by Cobble".to_string(),
output_dir,
functions: HashMap::new(),
command_metadata: HashMap::new(),
tags: HashMap::new(),
advancements: HashMap::new(),
loot_tables: HashMap::new(),
recipes: HashMap::new(),
predicates: HashMap::new(),
item_modifiers: HashMap::new(),
json_resources: HashMap::new(),
pack_format: SUPPORTED_PACK_FORMAT,
stdlib: StdLib::new(),
used_objectives: HashSet::new(),
}
}
pub fn set_description(&mut self, desc: String) {
self.description = desc;
}
pub fn set_pack_format(&mut self, format: PackFormat) {
self.pack_format = format;
}
fn metadata_for_commands(
commands: &[String],
kind: GeneratedCommandKind,
) -> HashMap<usize, GeneratedCommand> {
commands
.iter()
.enumerate()
.map(|(index, command)| {
(
index,
GeneratedCommand::new(command.clone(), None, kind.clone()),
)
})
.collect()
}
fn complete_metadata(
commands: &[String],
mut metadata: HashMap<usize, GeneratedCommand>,
default_kind: GeneratedCommandKind,
) -> HashMap<usize, GeneratedCommand> {
for (index, command) in commands.iter().enumerate() {
metadata.entry(index).or_insert_with(|| {
GeneratedCommand::new(command.clone(), None, default_kind.clone())
});
}
metadata
}
pub fn add_function(&mut self, name: String, commands: Vec<String>) {
self.add_function_with_kind(name, commands, GeneratedCommandKind::ControlFlow);
}
pub fn add_function_with_kind(
&mut self,
name: String,
commands: Vec<String>,
kind: GeneratedCommandKind,
) {
let metadata = Self::metadata_for_commands(&commands, kind);
self.command_metadata.insert(name.clone(), metadata);
self.functions.insert(name, commands);
}
pub fn add_function_with_metadata(
&mut self,
name: String,
commands: Vec<String>,
metadata: HashMap<usize, GeneratedCommand>,
) {
let metadata =
Self::complete_metadata(&commands, metadata, GeneratedCommandKind::ControlFlow);
self.command_metadata.insert(name.clone(), metadata);
self.functions.insert(name, commands);
}
pub fn insert_function_commands_with_kind(
&mut self,
name: &str,
index: usize,
new_commands: &[String],
kind: GeneratedCommandKind,
) {
if new_commands.is_empty() {
return;
}
let Some(commands) = self.functions.get_mut(name) else {
return;
};
let insert_index = index.min(commands.len());
for (offset, command) in new_commands.iter().enumerate() {
commands.insert(insert_index + offset, command.clone());
}
let existing_metadata = self.command_metadata.remove(name).unwrap_or_default();
let mut updated_metadata = HashMap::new();
for (old_index, metadata) in existing_metadata {
let new_index = if old_index >= insert_index {
old_index + new_commands.len()
} else {
old_index
};
updated_metadata.insert(new_index, metadata);
}
for (offset, command) in new_commands.iter().enumerate() {
updated_metadata.insert(
insert_index + offset,
GeneratedCommand::new(command.clone(), None, kind.clone()),
);
}
let completed = Self::complete_metadata(
commands,
updated_metadata,
GeneratedCommandKind::ControlFlow,
);
self.command_metadata.insert(name.to_string(), completed);
}
pub fn insert_function_commands_with_metadata(
&mut self,
name: &str,
index: usize,
new_commands: &[String],
new_metadata: HashMap<usize, GeneratedCommand>,
) {
if new_commands.is_empty() {
return;
}
let Some(commands) = self.functions.get_mut(name) else {
return;
};
let insert_index = index.min(commands.len());
for (offset, command) in new_commands.iter().enumerate() {
commands.insert(insert_index + offset, command.clone());
}
let existing_metadata = self.command_metadata.remove(name).unwrap_or_default();
let mut updated_metadata = HashMap::new();
for (old_index, metadata) in existing_metadata {
let new_index = if old_index >= insert_index {
old_index + new_commands.len()
} else {
old_index
};
updated_metadata.insert(new_index, metadata);
}
for (offset, command) in new_commands.iter().enumerate() {
let generated = new_metadata.get(&offset).cloned().unwrap_or_else(|| {
GeneratedCommand::new(command.clone(), None, GeneratedCommandKind::ControlFlow)
});
updated_metadata.insert(insert_index + offset, generated);
}
let completed = Self::complete_metadata(
commands,
updated_metadata,
GeneratedCommandKind::ControlFlow,
);
self.command_metadata.insert(name.to_string(), completed);
}
pub fn track_objective(&mut self, objective: &str) {
self.used_objectives.insert(objective.to_string());
}
pub fn ensure_init_function(&mut self) {
let load_handlers = self.stdlib.get_event_handlers(&EventType::Load);
if let Some(init_func_name) = load_handlers.first().cloned() {
if let Some(commands) = self.functions.get_mut(&init_func_name) {
let mut setup_commands = Vec::new();
let gamerule_cmd = "gamerule max_command_sequence_length 1000000000".to_string();
if !commands.contains(&gamerule_cmd) {
setup_commands.push(gamerule_cmd);
}
let mut objectives: Vec<_> = self.used_objectives.iter().collect();
objectives.sort();
for objective in objectives {
let obj_cmd = format!("scoreboard objectives add {} dummy", objective);
if !commands.contains(&obj_cmd) {
setup_commands.push(obj_cmd);
}
}
if self.used_objectives.contains("__internal__") {
let internal_init_1 =
"scoreboard players set #true_const __internal__ 1".to_string();
let internal_init_2 =
"scoreboard players set #false_const __internal__ 0".to_string();
if !commands.contains(&internal_init_1) {
setup_commands.push(internal_init_1);
}
if !commands.contains(&internal_init_2) {
setup_commands.push(internal_init_2);
}
}
if !setup_commands.is_empty() {
let inserted_count = setup_commands.len();
let mut updated_commands = setup_commands;
updated_commands.extend(commands.clone());
let mut updated_metadata = Self::metadata_for_commands(
&updated_commands[..inserted_count],
GeneratedCommandKind::RuntimeSetup,
);
if let Some(existing_metadata) = self.command_metadata.remove(&init_func_name) {
for (index, command) in existing_metadata {
updated_metadata.insert(index + inserted_count, command);
}
}
updated_metadata = Self::complete_metadata(
&updated_commands,
updated_metadata,
GeneratedCommandKind::ControlFlow,
);
self.command_metadata
.insert(init_func_name.clone(), updated_metadata);
*commands = updated_commands;
}
}
} else if !self.used_objectives.is_empty() {
let mut commands = Vec::new();
commands.push("gamerule max_command_sequence_length 1000000000".to_string());
let mut objectives: Vec<_> = self.used_objectives.iter().collect();
objectives.sort();
for objective in objectives {
commands.push(format!("scoreboard objectives add {} dummy", objective));
}
if self.used_objectives.contains("__internal__") {
commands.push("scoreboard players set #true_const __internal__ 1".to_string());
commands.push("scoreboard players set #false_const __internal__ 0".to_string());
}
self.add_function_with_kind(
"_cobble_init".to_string(),
commands,
GeneratedCommandKind::RuntimeSetup,
);
self.stdlib
.add_event_listener(EventType::Load, "_cobble_init".to_string());
}
}
pub fn add_tag(&mut self, tag_name: String, functions: Vec<String>) {
self.tags.insert(tag_name, functions);
}
pub fn add_advancement(&mut self, name: String, json: String) {
self.advancements.insert(name, json);
}
pub fn add_loot_table(&mut self, name: String, json: String) {
self.loot_tables.insert(name, json);
}
pub fn add_recipe(&mut self, name: String, json: String) {
self.recipes.insert(name, json);
}
pub fn add_predicate(&mut self, name: String, json: String) {
self.predicates.insert(name, json);
}
pub fn add_item_modifier(&mut self, name: String, json: String) {
self.item_modifiers.insert(name, json);
}
pub fn add_json_resource(&mut self, relative_path: String, json: String) -> Result<(), String> {
self.add_json_resource_in_namespace(self.namespace.clone(), relative_path, json)
}
pub fn add_json_resource_in_namespace(
&mut self,
namespace: String,
relative_path: String,
json: String,
) -> Result<(), String> {
let key = Self::json_resource_key(&namespace, &relative_path);
if self.json_resources.contains_key(&key) {
return Err(format!(
"Duplicate data pack resource '{}:{}'",
namespace, relative_path
));
}
self.json_resources.insert(key, json);
Ok(())
}
pub fn write(&self) -> std::io::Result<()> {
let data_dir = self.output_dir.join("data");
let namespace_dir = data_dir.join(&self.namespace);
let function_dir = namespace_dir.join("function");
let legacy_function_dir = namespace_dir.join("functions");
let minecraft_tags_dir = self
.output_dir
.join("data")
.join("minecraft")
.join("tags")
.join("function");
let legacy_minecraft_tags_dir = self
.output_dir
.join("data")
.join("minecraft")
.join("tags")
.join("functions");
let source_map_dir = self.output_dir.join(".cobble");
let generated_namespaces_path = source_map_dir.join("generated_namespaces.json");
let generated_namespaces = self.generated_namespaces();
if let Ok(content) = fs::read_to_string(&generated_namespaces_path) {
if let Ok(previous_namespaces) = serde_json::from_str::<Vec<String>>(&content) {
for namespace in previous_namespaces {
if !Self::is_safe_namespace_path(&namespace) {
continue;
}
if !generated_namespaces.contains(&namespace) {
let old_namespace_dir = data_dir.join(namespace);
if old_namespace_dir.exists() {
fs::remove_dir_all(old_namespace_dir)?;
}
} else if namespace != self.namespace {
Self::clean_generated_function_dirs(&data_dir.join(namespace))?;
}
}
}
}
if function_dir.exists() {
fs::remove_dir_all(&function_dir)?;
}
if legacy_function_dir.exists() {
fs::remove_dir_all(&legacy_function_dir)?;
}
if source_map_dir.exists() {
fs::remove_dir_all(&source_map_dir)?;
}
if minecraft_tags_dir.exists() {
fs::remove_dir_all(&minecraft_tags_dir)?;
}
if legacy_minecraft_tags_dir.exists() {
fs::remove_dir_all(&legacy_minecraft_tags_dir)?;
}
for namespace in &generated_namespaces {
Self::clean_generated_resource_dirs(&data_dir.join(namespace))?;
}
fs::create_dir_all(&function_dir)?;
self.write_pack_mcmeta()?;
let mut source_map_entries = Vec::new();
let mut function_names: Vec<_> = self.functions.keys().collect();
function_names.sort();
for name in function_names {
let commands = &self.functions[name];
let file_path = function_dir.join(format!("{}.mcfunction", name));
let mut file = fs::File::create(file_path)?;
let generated_path = format!("data/{}/function/{}.mcfunction", self.namespace, name);
for (index, command) in commands.iter().enumerate() {
writeln!(file, "{}", command)?;
let metadata = self
.command_metadata
.get(name)
.and_then(|commands| commands.get(&index))
.cloned()
.unwrap_or_else(|| {
GeneratedCommand::new(
command.clone(),
None,
GeneratedCommandKind::ControlFlow,
)
});
source_map_entries.push(SourceMapEntry {
generated_path: generated_path.clone(),
generated_line: index + 1,
command: metadata.text,
source: metadata.source,
kind: metadata.kind,
});
}
}
if !source_map_entries.is_empty() {
let source_map = SourceMap {
version: 1,
entries: source_map_entries,
};
fs::create_dir_all(&source_map_dir)?;
fs::write(
source_map_dir.join("source_map.json"),
serde_json::to_string_pretty(&source_map).unwrap(),
)?;
}
fs::create_dir_all(&source_map_dir)?;
fs::write(
source_map_dir.join("generated_namespaces.json"),
serde_json::to_string_pretty(&generated_namespaces).unwrap(),
)?;
let stdlib_tags = self.stdlib.generate_tags(&self.namespace);
for (tag_name, functions) in stdlib_tags {
Self::write_function_tag(&data_dir, &self.namespace, &tag_name, &functions)?;
}
let mut custom_tag_names: Vec<_> = self.tags.keys().collect();
custom_tag_names.sort();
for tag_name in custom_tag_names {
Self::write_function_tag(&data_dir, &self.namespace, tag_name, &self.tags[tag_name])?;
}
if !self.advancements.is_empty() {
let advancement_dir = namespace_dir.join("advancement");
for (name, json) in &self.advancements {
Self::write_json_resource_file(&advancement_dir, name, json)?;
}
}
if !self.loot_tables.is_empty() {
let loot_table_dir = namespace_dir.join("loot_table");
for (name, json) in &self.loot_tables {
Self::write_json_resource_file(&loot_table_dir, name, json)?;
}
}
if !self.recipes.is_empty() {
let recipe_dir = namespace_dir.join("recipe");
for (name, json) in &self.recipes {
Self::write_json_resource_file(&recipe_dir, name, json)?;
}
}
if !self.predicates.is_empty() {
let predicate_dir = namespace_dir.join("predicate");
for (name, json) in &self.predicates {
Self::write_json_resource_file(&predicate_dir, name, json)?;
}
}
if !self.item_modifiers.is_empty() {
let item_modifier_dir = namespace_dir.join("item_modifier");
for (name, json) in &self.item_modifiers {
Self::write_json_resource_file(&item_modifier_dir, name, json)?;
}
}
let mut json_resource_paths: Vec<_> = self.json_resources.keys().collect();
json_resource_paths.sort();
for key in json_resource_paths {
let json = &self.json_resources[key];
let Some((resource_namespace, relative_path)) = Self::split_json_resource_key(key)
else {
continue;
};
let file_path = data_dir
.join(resource_namespace)
.join(format!("{}.json", relative_path));
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let content = Self::merge_tag_json_if_existing(&file_path, relative_path, json)?;
fs::write(file_path, content)?;
}
Ok(())
}
fn is_safe_namespace_path(namespace: &str) -> bool {
!namespace.is_empty()
&& namespace.chars().all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' || c == '.'
})
}
fn json_resource_key(namespace: &str, relative_path: &str) -> String {
format!("{}/{}", namespace, relative_path)
}
fn split_json_resource_key(key: &str) -> Option<(&str, &str)> {
key.split_once('/')
}
fn generated_namespaces(&self) -> Vec<String> {
let mut namespaces = HashSet::new();
namespaces.insert(self.namespace.clone());
namespaces.insert("minecraft".to_string());
for key in self.json_resources.keys() {
if let Some((namespace, _)) = Self::split_json_resource_key(key) {
namespaces.insert(namespace.to_string());
}
}
for tag_name in self.tags.keys() {
if let Some((namespace, _)) = tag_name.split_once(':') {
namespaces.insert(namespace.to_string());
}
}
let mut namespaces = namespaces.into_iter().collect::<Vec<_>>();
namespaces.sort();
namespaces
}
fn clean_generated_resource_dirs(namespace_dir: &Path) -> std::io::Result<()> {
let tags_dir = namespace_dir.join("tags");
if tags_dir.exists() {
fs::remove_dir_all(tags_dir)?;
}
for resource_dir in [
"advancement",
"advancements",
"loot_table",
"loot_tables",
"recipe",
"recipes",
"predicate",
"predicates",
"item_modifier",
"item_modifiers",
"dialog",
] {
let path = namespace_dir.join(resource_dir);
if path.exists() {
fs::remove_dir_all(path)?;
}
}
Ok(())
}
fn clean_generated_function_dirs(namespace_dir: &Path) -> std::io::Result<()> {
for function_dir in ["function", "functions"] {
let path = namespace_dir.join(function_dir);
if path.exists() {
fs::remove_dir_all(path)?;
}
}
Ok(())
}
fn merge_tag_json_if_existing(
file_path: &Path,
relative_path: &str,
new_json: &str,
) -> std::io::Result<String> {
if !relative_path.starts_with("tags/") || !file_path.exists() {
return Ok(new_json.to_string());
}
let Ok(existing_content) = fs::read_to_string(file_path) else {
return Ok(new_json.to_string());
};
let Ok(mut merged_value) = serde_json::from_str::<serde_json::Value>(&existing_content)
else {
return Ok(new_json.to_string());
};
let Ok(new_value) = serde_json::from_str::<serde_json::Value>(new_json) else {
return Ok(new_json.to_string());
};
let Some(merged_values) = merged_value
.get_mut("values")
.and_then(|value| value.as_array_mut())
else {
return Ok(new_json.to_string());
};
let Some(new_values) = new_value.get("values").and_then(|value| value.as_array()) else {
return Ok(new_json.to_string());
};
for new_value in new_values {
if !merged_values.contains(new_value) {
merged_values.push(new_value.clone());
}
}
serde_json::to_string_pretty(&merged_value)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
fn write_function_tag(
data_dir: &Path,
default_namespace: &str,
tag_name: &str,
functions: &[String],
) -> std::io::Result<()> {
let (namespace, relative_tag_name) =
if let Some((namespace, path)) = tag_name.split_once(':') {
(namespace, path)
} else {
(default_namespace, tag_name)
};
let tag_file = data_dir
.join(namespace)
.join("tags")
.join("function")
.join(format!("{}.json", relative_tag_name));
let tag_content = json!({
"values": functions
});
if let Some(parent) = tag_file.parent() {
fs::create_dir_all(parent)?;
}
let mut file = fs::File::create(tag_file)?;
writeln!(
file,
"{}",
serde_json::to_string_pretty(&tag_content).unwrap()
)?;
Ok(())
}
fn write_json_resource_file(
resource_dir: &Path,
name: &str,
json: &str,
) -> std::io::Result<()> {
let file_path = resource_dir.join(format!("{}.json", name));
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(file_path, json)
}
fn write_pack_mcmeta(&self) -> std::io::Result<()> {
let mcmeta_path = self.output_dir.join("pack.mcmeta");
let mcmeta_content = match self.pack_format {
PackFormat::Decimal(major, minor) => {
json!({
"pack": {
"description": self.description,
"min_format": [major, minor],
"max_format": [major, minor]
}
})
}
PackFormat::Integer(v) => {
json!({
"pack": {
"pack_format": v,
"description": self.description
}
})
}
};
let mut file = fs::File::create(mcmeta_path)?;
writeln!(
file,
"{}",
serde_json::to_string_pretty(&mcmeta_content).unwrap()
)?;
Ok(())
}
pub fn ensure_math_helper(&mut self, op: &str) {
let func_name = format!("_cobble_math_{}", op);
if self.functions.contains_key(&func_name) {
return;
}
let mut commands = Vec::new();
match op {
"abs" => {
commands.push(
"scoreboard players operation #math_result math = #math_input math".to_string(),
);
commands.push("scoreboard players set #math_temp math -1".to_string());
commands.push("execute if score #math_result math matches ..-1 run scoreboard players operation #math_result math *= #math_temp math".to_string());
}
"min" => {
commands.push(
"scoreboard players operation #math_result math = #math_input math".to_string(),
);
commands.push("execute if score #math_input2 math < #math_result math run scoreboard players operation #math_result math = #math_input2 math".to_string());
}
"max" => {
commands.push(
"scoreboard players operation #math_result math = #math_input math".to_string(),
);
commands.push("execute if score #math_input2 math > #math_result math run scoreboard players operation #math_result math = #math_input2 math".to_string());
}
"sqrt" => {
commands.push("scoreboard players set #math_result math 0".to_string());
commands.push(
"scoreboard players operation #sqrt_high math = #math_input math".to_string(),
);
commands.push("scoreboard players set #sqrt_low math 0".to_string());
commands.push("scoreboard players set #sqrt_limit math 46340".to_string());
commands.push("scoreboard players set #sqrt_two math 2".to_string());
commands.push(
"execute if score #sqrt_high math matches ..-1 run scoreboard players set #sqrt_high math 0"
.to_string(),
);
commands.push(
"execute if score #sqrt_high math > #sqrt_limit math run scoreboard players operation #sqrt_high math = #sqrt_limit math"
.to_string(),
);
for _ in 0..16 {
commands.push(
"scoreboard players operation #sqrt_mid math = #sqrt_low math".to_string(),
);
commands.push(
"scoreboard players operation #sqrt_mid math += #sqrt_high math"
.to_string(),
);
commands.push(
"scoreboard players operation #sqrt_mid math /= #sqrt_two math".to_string(),
);
commands.push(
"scoreboard players operation #sqrt_square math = #sqrt_mid math"
.to_string(),
);
commands.push(
"scoreboard players operation #sqrt_square math *= #sqrt_mid math"
.to_string(),
);
commands.push(
"execute if score #sqrt_square math <= #math_input math run scoreboard players operation #math_result math = #sqrt_mid math"
.to_string(),
);
commands.push(
"execute if score #sqrt_square math <= #math_input math run scoreboard players operation #sqrt_low math = #sqrt_mid math"
.to_string(),
);
commands.push(
"execute if score #sqrt_square math <= #math_input math run scoreboard players add #sqrt_low math 1"
.to_string(),
);
commands.push(
"execute if score #sqrt_square math > #math_input math run scoreboard players operation #sqrt_high math = #sqrt_mid math"
.to_string(),
);
commands.push(
"execute if score #sqrt_square math > #math_input math run scoreboard players remove #sqrt_high math 1"
.to_string(),
);
}
}
_ => {}
}
self.add_function_with_kind(func_name, commands, GeneratedCommandKind::StdLib);
}
}