376 lines
12 KiB
Rust
376 lines
12 KiB
Rust
use anyhow::Result;
|
|
use path_absolutize::Absolutize as _;
|
|
use shulkerbox::virtual_fs::{VFile, VFolder};
|
|
use std::{
|
|
borrow::Cow,
|
|
fs::{self, File},
|
|
io::BufReader,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use walkdir::WalkDir;
|
|
|
|
use crate::{
|
|
terminal_output::{print_error, print_info, print_success},
|
|
util::Relativize as _,
|
|
};
|
|
|
|
#[derive(Debug, clap::Args, Clone)]
|
|
#[command(allow_missing_positional = true)]
|
|
pub struct MigrateArgs {
|
|
/// The path of the project to migrate.
|
|
#[arg(default_value = ".")]
|
|
pub path: PathBuf,
|
|
/// The path of the folder to create the ShulkerScript project.
|
|
pub target: PathBuf,
|
|
/// Force migration even if some features will be lost.
|
|
#[arg(short, long)]
|
|
pub force: bool,
|
|
}
|
|
|
|
pub fn migrate(args: &MigrateArgs) -> Result<()> {
|
|
let base_path = args.path.as_path();
|
|
let base_path = if base_path.is_absolute() {
|
|
Cow::Borrowed(base_path)
|
|
} else {
|
|
base_path.absolutize().unwrap_or(Cow::Borrowed(base_path))
|
|
}
|
|
.ancestors()
|
|
.find(|p| p.join("pack.mcmeta").exists())
|
|
.map(|p| p.relativize().unwrap_or_else(|| p.to_path_buf()));
|
|
|
|
if let Some(base_path) = base_path {
|
|
print_info(format!(
|
|
"Migrating from {:?} to {:?}",
|
|
base_path, args.target
|
|
));
|
|
|
|
let mcmeta_path = base_path.join("pack.mcmeta");
|
|
let mcmeta: serde_json::Value =
|
|
serde_json::from_reader(BufReader::new(fs::File::open(&mcmeta_path)?))?;
|
|
|
|
if !args.force && !is_mcmeta_compatible(&mcmeta) {
|
|
print_error("Your datapack uses features in the pack.mcmeta file that are not yet supported by ShulkerScript.");
|
|
print_error(
|
|
r#""features", "filter", "overlays" and "language" will get lost if you continue."#,
|
|
);
|
|
print_error("Use the force flag to continue anyway.");
|
|
|
|
return Err(anyhow::anyhow!("Incompatible mcmeta."));
|
|
}
|
|
|
|
let mcmeta = serde_json::from_value::<McMeta>(mcmeta)?;
|
|
|
|
let mut root = VFolder::new();
|
|
root.add_file("pack.toml", generate_pack_toml(&base_path, &mcmeta)?);
|
|
|
|
let data_path = base_path.join("data");
|
|
if data_path.exists() && data_path.is_dir() {
|
|
for namespace in data_path.read_dir()? {
|
|
let namespace = namespace?;
|
|
if namespace.file_type()?.is_dir() {
|
|
handle_namespace(&mut root, &namespace.path())?;
|
|
}
|
|
}
|
|
} else {
|
|
print_error("Could not find a data folder.");
|
|
}
|
|
|
|
root.place(&args.target)?;
|
|
|
|
print_success("Migration successful.");
|
|
Ok(())
|
|
} else {
|
|
let msg = format!(
|
|
"Could not find a valid datapack to migrate at {}.",
|
|
args.path.display()
|
|
);
|
|
print_error(&msg);
|
|
Err(anyhow::anyhow!("{}", &msg))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
|
struct McMeta {
|
|
pack: McMetaPack,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
|
struct McMetaPack {
|
|
description: String,
|
|
pack_format: u8,
|
|
}
|
|
|
|
fn is_mcmeta_compatible(mcmeta: &serde_json::Value) -> bool {
|
|
mcmeta.as_object().map_or(false, |mcmeta| {
|
|
mcmeta.len() == 1
|
|
&& mcmeta.contains_key("pack")
|
|
&& mcmeta["pack"]
|
|
.as_object()
|
|
.is_some_and(|pack| !pack.contains_key("supported_formats"))
|
|
})
|
|
}
|
|
|
|
fn generate_pack_toml(base_path: &Path, mcmeta: &McMeta) -> Result<VFile> {
|
|
// check if there are any directories in namespaces other than `functions`, `function` and `tags`
|
|
let mut err = false;
|
|
let requires_assets_dir = base_path.join("data").read_dir()?.any(|entry_i| {
|
|
if let Ok(entry_i) = entry_i {
|
|
if let Ok(metadata_i) = entry_i.metadata() {
|
|
metadata_i.is_dir()
|
|
&& entry_i
|
|
.path()
|
|
.read_dir()
|
|
.map(|mut dir| {
|
|
dir.any(|entry_ii| {
|
|
if let Ok(entry_ii) = entry_ii {
|
|
["functions", "function", "tags"]
|
|
.contains(&entry_ii.file_name().to_string_lossy().as_ref())
|
|
} else {
|
|
err = true;
|
|
true
|
|
}
|
|
})
|
|
})
|
|
.map_err(|e| {
|
|
err = true;
|
|
e
|
|
})
|
|
.unwrap_or_default()
|
|
} else {
|
|
err = true;
|
|
true
|
|
}
|
|
} else {
|
|
err = true;
|
|
true
|
|
}
|
|
});
|
|
|
|
if err {
|
|
print_error("Error reading data directory");
|
|
return Err(anyhow::anyhow!("Error reading data directory"));
|
|
}
|
|
|
|
let assets_dir_fragment = requires_assets_dir.then(|| {
|
|
toml::toml! {
|
|
[compiler]
|
|
assets = "./assets"
|
|
}
|
|
});
|
|
|
|
let name = base_path
|
|
.absolutize()?
|
|
.file_name()
|
|
.expect("No file name")
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
let description = mcmeta.pack.description.as_str();
|
|
let pack_format = mcmeta.pack.pack_format;
|
|
|
|
let main_fragment = toml::toml! {
|
|
[pack]
|
|
name = name
|
|
description = description
|
|
format = pack_format
|
|
version = "0.1.0"
|
|
};
|
|
|
|
let assets_dir_fragment_text = assets_dir_fragment
|
|
.map(|fragment| toml::to_string_pretty(&fragment))
|
|
.transpose()?;
|
|
|
|
// stringify the toml fragments and add them to the pack.toml file
|
|
toml::to_string_pretty(&main_fragment)
|
|
.map(|mut text| {
|
|
if let Some(assets_dir_fragment_text) = assets_dir_fragment_text {
|
|
text.push('\n');
|
|
text.push_str(&assets_dir_fragment_text);
|
|
}
|
|
VFile::Text(text)
|
|
})
|
|
.map_err(|e| e.into())
|
|
}
|
|
|
|
fn handle_namespace(root: &mut VFolder, namespace: &Path) -> Result<()> {
|
|
let namespace_name = namespace
|
|
.file_name()
|
|
.expect("path cannot end with ..")
|
|
.to_string_lossy();
|
|
|
|
// migrate all subfolders of namespace
|
|
for subfolder in namespace.read_dir()? {
|
|
let subfolder = subfolder?;
|
|
if !subfolder.file_type()?.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let filename = subfolder.file_name();
|
|
let filename = filename.to_string_lossy();
|
|
|
|
if ["function", "functions"].contains(&filename.as_ref()) {
|
|
// migrate functions
|
|
for entry in WalkDir::new(subfolder.path()).min_depth(1) {
|
|
let entry = entry?;
|
|
if entry.file_type().is_file()
|
|
&& entry.path().extension().unwrap_or_default() == "mcfunction"
|
|
{
|
|
handle_function(root, namespace, &namespace_name, entry.path())?;
|
|
}
|
|
}
|
|
} else if filename.as_ref() == "tags" {
|
|
// migrate tags
|
|
for tag_type in subfolder.path().read_dir()? {
|
|
handle_tag_type_dir(root, &namespace_name, &tag_type?.path())?;
|
|
}
|
|
} else {
|
|
// copy all other files to the asset folder
|
|
let vfolder = VFolder::try_from(subfolder.path().as_path())?;
|
|
root.add_existing_folder(&format!("assets/data/{namespace_name}/{filename}"), vfolder);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_function(
|
|
root: &mut VFolder,
|
|
namespace: &Path,
|
|
namespace_name: &str,
|
|
function: &Path,
|
|
) -> Result<()> {
|
|
let function_path = pathdiff::diff_paths(function, namespace.join("function"))
|
|
.expect("function path is always a subpath of namespace/function")
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
let function_path = function_path
|
|
.trim_start_matches("./")
|
|
.trim_end_matches(".mcfunction");
|
|
|
|
// indent lines and prefix comments with `///` and commands with `/`
|
|
let content = fs::read_to_string(function)?
|
|
.lines()
|
|
.map(|l| {
|
|
if l.trim_start().starts_with('#') {
|
|
format!(" {}", l.replacen('#', "///", 1))
|
|
} else if l.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" /{}", l)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let function_name = function_path
|
|
.split('/')
|
|
.last()
|
|
.expect("split always returns at least one element")
|
|
.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
|
|
|
|
// generate the full content of the function file
|
|
let full_content = indoc::formatdoc!(
|
|
r#"// This file was automatically migrated by ShulkerScript CLI v{version} from file "{function}"
|
|
namespace "{namespace_name}";
|
|
|
|
#[deobfuscate = "{function_path}"]
|
|
fn {function_name}() {{
|
|
{content}
|
|
}}
|
|
"#,
|
|
version = env!("CARGO_PKG_VERSION"),
|
|
function = function.display()
|
|
);
|
|
|
|
root.add_file(
|
|
&format!("src/functions/{namespace_name}/{function_path}.shu"),
|
|
VFile::Text(full_content),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_tag_type_dir(root: &mut VFolder, namespace: &str, tag_type_dir: &Path) -> Result<()> {
|
|
let tag_type = tag_type_dir
|
|
.file_name()
|
|
.expect("cannot end with ..")
|
|
.to_string_lossy();
|
|
|
|
// loop through all tag files in the tag type directory
|
|
for entry in WalkDir::new(tag_type_dir).min_depth(1) {
|
|
let entry = entry?;
|
|
if entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "json" {
|
|
handle_tag(root, namespace, tag_type_dir, &tag_type, entry.path())?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_tag(
|
|
root: &mut VFolder,
|
|
namespace: &str,
|
|
tag_type_dir: &Path,
|
|
tag_type: &str,
|
|
tag: &Path,
|
|
) -> Result<()> {
|
|
let tag_path = pathdiff::diff_paths(tag, tag_type_dir)
|
|
.expect("tag path is always a subpath of tag_type_dir")
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
let tag_path = tag_path.trim_start_matches("./").trim_end_matches(".json");
|
|
|
|
if let Ok(content) = serde_json::from_reader::<_, Tag>(BufReader::new(File::open(tag)?)) {
|
|
// generate "of <type>" if the tag type is not "function"
|
|
let of_type = if tag_type == "function" {
|
|
String::new()
|
|
} else {
|
|
format!(r#" of "{tag_type}""#)
|
|
};
|
|
|
|
let replace = if content.replace { " replace" } else { "" };
|
|
|
|
// indent, quote and join the values
|
|
let values = content
|
|
.values
|
|
.iter()
|
|
.map(|t| format!(r#" "{t}""#))
|
|
.collect::<Vec<_>>()
|
|
.join(",\n");
|
|
|
|
let generated = indoc::formatdoc!(
|
|
r#"// This file was automatically migrated by ShulkerScript CLI v{version} from file "{tag}"
|
|
namespace "{namespace}";
|
|
|
|
tag "{tag_path}"{of_type}{replace} [
|
|
{values}
|
|
]
|
|
"#,
|
|
version = env!("CARGO_PKG_VERSION"),
|
|
tag = tag.display(),
|
|
);
|
|
|
|
root.add_file(
|
|
&format!("src/tags/{namespace}/{tag_type}/{tag_path}.shu"),
|
|
VFile::Text(generated),
|
|
);
|
|
|
|
Ok(())
|
|
} else {
|
|
print_error(format!(
|
|
"Could not read tag file at {}. Required attribute of entries is not yet supported",
|
|
tag.display()
|
|
));
|
|
Err(anyhow::anyhow!(
|
|
"Could not read tag file at {}",
|
|
tag.display()
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
|
struct Tag {
|
|
#[serde(default)]
|
|
replace: bool,
|
|
values: Vec<String>,
|
|
}
|