use crate::modrinth::{
File, MrPack, MrPackDependencies, Project, Version, extract_mrpack_overrides,
};
use crate::{MRPACK_INDEX_FILE_NAME, PackrinthError, PackrinthResult, ProjectTable};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};
use walkdir::WalkDir;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
pub const CURRENT_PACK_FORMAT: u16 = 1;
fn json_to_file<T, P>(json_value: &T, file: P) -> PackrinthResult<()>
where
T: ?Sized + Serialize + Debug,
P: AsRef<Path>,
{
let json = match serde_json_to_string_pretty(json_value) {
Ok(json) => json,
Err(error) => {
return Err(PackrinthError::FailedToSerialize {
error_message: error.to_string(),
});
}
};
if let Err(error) = fs::write(&file, json) {
return Err(PackrinthError::FailedToWriteFile {
path_to_write_to: file.as_ref().display().to_string(),
error_message: error.to_string(),
});
}
Ok(())
}
fn serde_json_to_string_pretty<T>(value: &T) -> Result<String, serde_json::Error>
where
T: ?Sized + Serialize,
{
let mut buf = Vec::new();
let formatter = serde_json::ser::PrettyFormatter::with_indent(b"\t");
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
value.serialize(&mut ser)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Modpack {
pub pack_format: u16,
pub name: String,
pub summary: String,
pub author: String,
pub require_all: bool,
pub auto_dependencies: bool,
pub branches: Vec<String>,
pub projects: IndexMap<String, ProjectSettings>,
#[serde(skip)]
pub directory: PathBuf,
#[serde(skip)]
pub modpack_config_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub version_overrides: Option<IndexMap<String, String>>,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub include_or_exclude: Option<IncludeOrExclude>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum IncludeOrExclude {
#[serde(rename = "include")]
Include(Vec<String>),
#[serde(rename = "exclude")]
Exclude(Vec<String>),
}
pub const BRANCH_CONFIG_FILE_NAME: &str = "branch.json";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BranchConfig {
pub version: String,
pub minecraft_version: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub acceptable_minecraft_versions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mod_loader: Option<MainLoader>,
#[serde(skip_serializing_if = "Option::is_none")]
pub loader_version: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub acceptable_loaders: Vec<Loader>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub manual_files: Vec<File>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MainLoader {
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
NeoForge,
#[serde(rename = "fabric")]
Fabric,
#[serde(rename = "quilt")]
Quilt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Loader {
#[serde(rename = "minecraft")]
Minecraft,
#[serde(rename = "fabric")]
Fabric,
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
NeoForge,
#[serde(rename = "quilt")]
Quilt,
#[serde(rename = "babric")]
Babric,
#[serde(rename = "bta-babric")]
BTABabric,
#[serde(rename = "java-agent")]
JavaAgent,
#[serde(rename = "legacy-fabric")]
LegacyFabric,
#[serde(rename = "liteloader")]
LiteLoader,
#[serde(rename = "modloader")]
RisugamisModLoader,
#[serde(rename = "nilloader")]
NilLoader,
#[serde(rename = "ornithe")]
Ornithe,
#[serde(rename = "rift")]
Rift,
#[serde(rename = "canvas")]
Canvas,
#[serde(rename = "iris")]
Iris,
#[serde(rename = "optifine")]
Optifine,
#[serde(rename = "vanilla")]
VanillaShader,
#[serde(rename = "bukkit")]
Bukkit,
#[serde(rename = "folia")]
Folia,
#[serde(rename = "paper")]
Paper,
#[serde(rename = "purpur")]
Purpur,
#[serde(rename = "spigot")]
Spigot,
#[serde(rename = "sponge")]
Sponge,
#[serde(rename = "bungeecord")]
BungeeCord,
#[serde(rename = "velocity")]
Velocity,
#[serde(rename = "waterfall")]
Waterfall,
}
pub const BRANCH_FILES_FILE_NAME: &str = ".branch_files.json";
pub const BRANCH_FILES_INFO: &str = "This file is managed by Packrinth and not intended for manual editing. You should, however, add it to your Git repository.";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BranchFiles {
info: String,
pub projects: Vec<BranchFilesProject>,
pub files: Vec<File>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BranchFilesProject {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
pub const MODPACK_CONFIG_FILE_NAME: &str = "modpack.json";
const MODRINTH_PACK_FORMAT: u16 = 1;
const GAME: &str = "minecraft";
pub const OVERRIDE_DIRS: [&str; 3] = ["overrides", "server-overrides", "client-overrides"];
impl Modpack {
pub fn new(directory: &Path, force: bool) -> PackrinthResult<Self> {
let modpack_config_path = directory.join(MODPACK_CONFIG_FILE_NAME);
if !force
&& let Ok(exists) = fs::exists(&modpack_config_path)
&& exists
{
return Err(PackrinthError::ModpackAlreadyExists {
directory: directory.display().to_string(),
});
}
match fs::metadata(directory) {
Ok(metadata) => {
if metadata.is_file() {
return Err(PackrinthError::PathIsFile {
path: directory.display().to_string(),
});
}
}
Err(_error) => {
if let Err(error) = fs::create_dir_all(directory) {
return Err(PackrinthError::FailedToCreateDir {
dir_to_create: directory.display().to_string(),
error_message: error.to_string(),
});
}
}
}
let modpack = Self {
directory: PathBuf::from(directory),
modpack_config_path: directory.join(MODPACK_CONFIG_FILE_NAME),
..Self::default()
};
Ok(modpack)
}
pub fn from_directory(directory: &Path) -> PackrinthResult<Self> {
let modpack_config_path = directory.join(MODPACK_CONFIG_FILE_NAME);
let config = match fs::read_to_string(&modpack_config_path) {
Ok(config) => config,
Err(error) => {
return Err(PackrinthError::FailedToReadToString {
path_to_read: modpack_config_path.display().to_string(),
error_message: error.to_string(),
});
}
};
let mut modpack: Modpack = match serde_json::from_str(&config) {
Ok(modpack) => modpack,
Err(error) => {
return Err(PackrinthError::FailedToParseConfigJson {
config_path: modpack_config_path.display().to_string(),
error_message: error.to_string(),
});
}
};
modpack.directory = PathBuf::from(directory);
modpack.modpack_config_path = modpack_config_path;
Ok(modpack)
}
pub fn add_projects(
&mut self,
projects: &[String],
version_overrides: &Option<IndexMap<String, String>>,
include_or_exclude: &Option<IncludeOrExclude>,
) {
for project in projects {
self.projects.insert(
String::from(project),
if include_or_exclude.clone().is_some() {
ProjectSettings {
version_overrides: version_overrides.clone(),
include_or_exclude: include_or_exclude.clone(),
}
} else {
ProjectSettings {
version_overrides: None,
include_or_exclude: None,
}
},
);
}
}
pub fn add_version_override(
&mut self,
project: &str,
branch: &str,
project_version_id: &str,
) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(version_overrides) = &mut project_settings.version_overrides {
version_overrides.insert(branch.to_string(), project_version_id.to_string());
} else {
project_settings.version_overrides = Some(IndexMap::from([(
branch.to_string(),
project_version_id.to_string(),
)]));
}
Ok(())
}
pub fn remove_version_override(&mut self, project: &str, branch: &str) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(version_overrides) = &mut project_settings.version_overrides {
if version_overrides.shift_remove(branch).is_none() {
Err(PackrinthError::OverrideDoesNotExist {
project: project.to_string(),
branch: branch.to_string(),
})
} else {
Ok(())
}
} else {
Err(PackrinthError::NoOverridesForProject {
project: project.to_string(),
})
}
}
pub fn remove_all_version_overrides(&mut self, project: &str) -> PackrinthResult<()> {
if let Some(project_settings) = self.projects.get_mut(project) {
project_settings.version_overrides = None;
Ok(())
} else {
Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
})
}
}
pub fn add_project_inclusions(
&mut self,
project: &str,
new_inclusions: &[String],
) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(include_or_exclude) = &mut project_settings.include_or_exclude {
if let IncludeOrExclude::Include(inclusions) = include_or_exclude {
for new_include in new_inclusions {
inclusions.push(new_include.clone());
}
} else {
return Err(PackrinthError::ProjectAlreadyHasExclusions {
project: project.to_string(),
});
}
} else {
project_settings.include_or_exclude =
Some(IncludeOrExclude::Include(Vec::from(new_inclusions)));
}
Ok(())
}
pub fn remove_project_inclusions(
&mut self,
project: &str,
inclusions_to_remove: &[String],
) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(include_or_exclude) = &mut project_settings.include_or_exclude
&& let IncludeOrExclude::Include(inclusions) = include_or_exclude
{
inclusions.retain(|x| !inclusions_to_remove.contains(x));
Ok(())
} else {
Err(PackrinthError::NoInclusionsForProject {
project: project.to_string(),
})
}
}
pub fn remove_all_project_inclusions(&mut self, project: &str) -> PackrinthResult<()> {
if let Some(project_settings) = self.projects.get_mut(project)
&& let Some(include_or_exclude) = &project_settings.include_or_exclude
{
if let IncludeOrExclude::Include(_) = include_or_exclude {
project_settings.include_or_exclude = None;
Ok(())
} else {
Err(PackrinthError::NoInclusionsForProject {
project: project.to_string(),
})
}
} else {
Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
})
}
}
pub fn add_project_exclusions(
&mut self,
project: &str,
new_exclusions: &[String],
) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(include_or_exclude) = &mut project_settings.include_or_exclude {
if let IncludeOrExclude::Exclude(exclusions) = include_or_exclude {
for new_exclude in new_exclusions {
exclusions.push(new_exclude.clone());
}
} else {
return Err(PackrinthError::ProjectAlreadyHasInclusions {
project: project.to_string(),
});
}
} else {
project_settings.include_or_exclude =
Some(IncludeOrExclude::Exclude(Vec::from(new_exclusions)));
}
Ok(())
}
pub fn remove_project_exclusions(
&mut self,
project: &str,
exclusions_to_remove: &[String],
) -> PackrinthResult<()> {
let Some(project_settings) = self.projects.get_mut(project) else {
return Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
});
};
if let Some(include_or_exclude) = &mut project_settings.include_or_exclude
&& let IncludeOrExclude::Exclude(exclusions) = include_or_exclude
{
exclusions.retain(|x| !exclusions_to_remove.contains(x));
Ok(())
} else {
Err(PackrinthError::NoExclusionsForProject {
project: project.to_string(),
})
}
}
pub fn remove_all_project_exclusions(&mut self, project: &str) -> PackrinthResult<()> {
if let Some(project_settings) = self.projects.get_mut(project)
&& let Some(include_or_exclude) = &project_settings.include_or_exclude
{
if let IncludeOrExclude::Exclude(_) = include_or_exclude {
project_settings.include_or_exclude = None;
Ok(())
} else {
Err(PackrinthError::NoExclusionsForProject {
project: project.to_string(),
})
}
} else {
Err(PackrinthError::ProjectIsNotAdded {
project: project.to_string(),
})
}
}
pub fn remove_projects(&mut self, projects: &[&str]) {
for project in projects {
self.projects.shift_remove(&(*project).to_string());
}
}
pub fn new_branch(&mut self, name: &str) -> PackrinthResult<BranchConfig> {
if !self.branches.contains(&name.to_string()) {
self.branches.push(name.to_string());
}
let branch_dir = self.directory.join(name);
if let Ok(exists) = fs::exists(&branch_dir)
&& !exists
&& let Err(error) = fs::create_dir(&branch_dir)
{
return Err(PackrinthError::FailedToCreateDir {
dir_to_create: branch_dir.display().to_string(),
error_message: error.to_string(),
});
}
BranchConfig::from_directory(&self.directory, name)
}
pub fn remove_branches(&mut self, branch_names: &Vec<String>) -> PackrinthResult<()> {
for branch_name in branch_names {
let branch_path = self.directory.join(branch_name);
if self.branches.contains(branch_name) {
self.branches.retain(|x| x != branch_name);
if let Ok(exists) = fs::exists(&branch_path)
&& exists
&& let Err(error) = fs::remove_dir_all(&branch_path)
{
return Err(PackrinthError::FailedToRemoveDir {
dir_to_remove: branch_path.display().to_string(),
error_message: error.to_string(),
});
}
}
}
Ok(())
}
pub fn save(&self) -> PackrinthResult<()> {
json_to_file(self, &self.modpack_config_path)
}
#[allow(clippy::too_many_lines)]
pub fn export_branch(&self, branch: &str) -> PackrinthResult<PathBuf> {
let branch_config = BranchConfig::from_directory(&self.directory, branch)?;
let branch_files = BranchFiles::from_directory(&self.directory, branch)?;
let mrpack_file_name = format!("{}_{}.mrpack", self.name, branch_config.version);
let branch_dir = self.directory.join(branch);
let target_dir = self.directory.join(crate::TARGET_DIRECTORY).join(branch);
if let Err(error) = fs::create_dir_all(&target_dir) {
return Err(PackrinthError::FailedToCreateDir {
dir_to_create: target_dir.display().to_string(),
error_message: error.to_string(),
});
}
let mrpack_path = target_dir.join(&mrpack_file_name);
let mrpack = MrPack {
format_version: MODRINTH_PACK_FORMAT,
game: GAME.to_string(),
version_id: branch_config.version.clone(),
name: self.name.clone(),
summary: Some(self.summary.clone()),
files: branch_files.files,
dependencies: Self::create_dependencies(branch_config)?,
};
let mrpack_json = match serde_json_to_string_pretty(&mrpack) {
Ok(mrpack_json) => mrpack_json,
Err(error) => {
return Err(PackrinthError::FailedToSerialize {
error_message: error.to_string(),
});
}
};
let options = SimpleFileOptions::default();
let zip_file = match fs::File::create(&mrpack_path) {
Ok(zip_file) => zip_file,
Err(error) => {
return Err(PackrinthError::FailedToInitializeFileType {
file_to_create: mrpack_path.display().to_string(),
error_message: error.to_string(),
});
}
};
let mut zip = ZipWriter::new(zip_file);
if let Err(error) = zip.start_file(MRPACK_INDEX_FILE_NAME, options) {
return Err(PackrinthError::FailedToStartZipFile {
file_to_start: MRPACK_INDEX_FILE_NAME.to_string(),
error_message: error.to_string(),
});
}
if let Err(error) = zip.write_all(mrpack_json.as_bytes()) {
return Err(PackrinthError::FailedToWriteToZip {
to_write: MRPACK_INDEX_FILE_NAME.to_string(),
error_message: error.to_string(),
});
}
let mut result = Ok(());
for override_dir in OVERRIDE_DIRS {
let override_dir_path = branch_dir.join(override_dir);
if let Ok(exists) = fs::exists(&override_dir_path)
&& !exists
{
continue;
}
for entry in WalkDir::new(override_dir_path) {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
result = Err(PackrinthError::FailedToGetWalkDirEntry {
error_message: error.to_string(),
});
continue;
}
};
let path = entry.path();
let zip_path = if let Ok(stripped_path) = path.strip_prefix(&branch_dir)
&& let Some(zip_path) = stripped_path.to_str()
{
zip_path
} else {
result = Err(PackrinthError::FailedToStripPath {
path: path.display().to_string(),
});
continue;
};
if path.is_file() {
if let Err(error) = zip.start_file(zip_path, options) {
result = Err(PackrinthError::FailedToStartZipFile {
file_to_start: zip_path.to_string(),
error_message: error.to_string(),
});
continue;
}
let mut buffer = Vec::new();
let mut original_file = match fs::File::open(path) {
Ok(file) => file,
Err(error) => {
result = Err(PackrinthError::FailedToInitializeFileType {
file_to_create: path.display().to_string(),
error_message: error.to_string(),
});
continue;
}
};
if let Err(_error) = io::copy(&mut original_file, &mut buffer) {
result = Err(PackrinthError::FailedToCopyIntoBuffer);
continue;
}
if let Err(error) = zip.write_all(&buffer) {
result = Err(PackrinthError::FailedToWriteToZip {
to_write: String::from_utf8_lossy(&buffer).to_string(),
error_message: error.to_string(),
});
}
} else if path.is_dir()
&& let Err(_error) = zip.add_directory(zip_path, options)
{
result = Err(PackrinthError::FailedToAddZipDir {
zip_dir_path: zip_path.to_string(),
});
}
}
}
if let Err(_error) = zip.finish() {
result = Err(PackrinthError::FailedToFinishZip);
}
match result {
Ok(()) => Ok(mrpack_path),
Err(error) => Err(error),
}
}
pub fn generate_project_table(&self) -> PackrinthResult<ProjectTable> {
let mut column_names = vec!["Name".to_string()];
let mut project_map: HashMap<BranchFilesProject, HashMap<String, Option<()>>> =
HashMap::new();
for branch in &self.branches {
column_names.push(branch.clone());
let branch_files = BranchFiles::from_directory(&self.directory, branch)?;
for project in &branch_files.projects {
if let Some(branch_map) = project_map.get_mut(project) {
if branch_map.get(branch).is_none() {
branch_map.insert(branch.clone(), Some(()));
}
} else {
let mut branch_map = HashMap::new();
branch_map.insert(branch.clone(), Some(()));
project_map.insert(project.clone(), branch_map);
}
}
}
for project in &mut project_map {
for branch in &self.branches {
if project.1.get(branch).is_none() {
project.1.insert(branch.clone(), None);
}
}
}
Ok(ProjectTable {
column_names,
project_map,
})
}
pub fn import_mrpack<F>(
&mut self,
mrpack: MrPack,
mrpack_path: &Path,
add_projects: bool,
force: bool,
mut f: F,
) -> PackrinthResult<()>
where
F: FnMut(String),
{
let branch_name = match mrpack_path.file_name() {
Some(branch_name) => branch_name.display().to_string(),
None => mrpack_path.display().to_string(),
}
.split(".mrpack")
.collect::<Vec<&str>>()[0]
.to_string();
if self.branches.contains(&branch_name) && !force {
return Err(PackrinthError::BranchAlreadyExists {
branch: branch_name,
});
}
let mut branch_config = self.new_branch(&branch_name)?;
branch_config.version.clone_from(&branch_name);
branch_config.minecraft_version = mrpack.dependencies.minecraft;
branch_config.acceptable_minecraft_versions = Vec::new();
if let Some(loader_version) = mrpack.dependencies.fabric_loader {
branch_config.mod_loader = Some(MainLoader::Fabric);
branch_config.loader_version = Some(loader_version);
} else if let Some(loader_version) = mrpack.dependencies.forge {
branch_config.mod_loader = Some(MainLoader::Forge);
branch_config.loader_version = Some(loader_version);
} else if let Some(loader_version) = mrpack.dependencies.neoforge {
branch_config.mod_loader = Some(MainLoader::NeoForge);
branch_config.loader_version = Some(loader_version);
} else if let Some(loader_version) = mrpack.dependencies.quilt_loader {
branch_config.mod_loader = Some(MainLoader::Quilt);
branch_config.loader_version = Some(loader_version);
}
branch_config.save(&self.directory, &branch_name)?;
let mut branch_files = BranchFiles::from_directory(&self.directory, &branch_name)?;
branch_files.files.clone_from(&mrpack.files);
for file in mrpack.files {
let version = match Version::from_sha512_hash(&file.hashes.sha512) {
Ok(version) => version,
Err(_error) => continue,
};
let project = Project::from_id(&version.project_id)?;
branch_files.projects.push(BranchFilesProject {
name: project.title,
id: Some(project.slug.clone()),
});
if add_projects
&& (!self.projects.contains_key(&version.project_id)
|| !self.projects.contains_key(&project.slug))
{
self.projects.insert(
project.slug.clone(),
ProjectSettings {
version_overrides: None,
include_or_exclude: None,
},
);
}
f(project.slug);
}
branch_files.save(&self.directory, &branch_name)?;
let mrpack_output = &self.directory.join(&branch_name);
if let Err(error) = extract_mrpack_overrides(mrpack_path, mrpack_output) {
return Err(PackrinthError::FailedToExtractMrPack {
mrpack_path: mrpack_path.display().to_string(),
output_directory: mrpack_output.display().to_string(),
error_message: error.to_string(),
});
}
self.save()?;
Ok(())
}
fn create_dependencies(branch_config: BranchConfig) -> PackrinthResult<MrPackDependencies> {
let mut forge = None;
let mut neoforge = None;
let mut fabric_loader = None;
let mut quilt_loader = None;
if let Some(main_loader) = branch_config.mod_loader {
let Some(loader_version) = branch_config.loader_version else {
return Err(PackrinthError::MainModLoaderProvidedButNoVersion);
};
match main_loader {
MainLoader::Forge => forge = Some(loader_version),
MainLoader::NeoForge => neoforge = Some(loader_version),
MainLoader::Fabric => fabric_loader = Some(loader_version),
MainLoader::Quilt => quilt_loader = Some(loader_version),
}
}
Ok(MrPackDependencies {
minecraft: branch_config.minecraft_version,
forge,
neoforge,
fabric_loader,
quilt_loader,
})
}
}
impl Default for Modpack {
fn default() -> Self {
Self {
pack_format: CURRENT_PACK_FORMAT,
name: "My Modrinth modpack".to_string(),
summary: "Short summary for this modpack".to_string(),
author: "John Doe".to_string(),
require_all: false,
auto_dependencies: true,
branches: Vec::default(),
projects: IndexMap::default(),
directory: PathBuf::default(),
modpack_config_path: PathBuf::default(),
}
}
}
impl BranchConfig {
pub fn from_directory(directory: &Path, name: &str) -> PackrinthResult<Self> {
let branch_dir = directory.join(name);
match fs::metadata(&branch_dir) {
Ok(metadata) => {
if metadata.is_dir() {
let branch_config_path = branch_dir.join(BRANCH_CONFIG_FILE_NAME);
let branch_config = match fs::read_to_string(&branch_config_path) {
Ok(contents) => {
let branch_config: Self = match serde_json::from_str(&contents) {
Ok(contents) => contents,
Err(error) => {
return Err(PackrinthError::FailedToParseConfigJson {
config_path: branch_config_path.display().to_string(),
error_message: error.to_string(),
});
}
};
branch_config
}
Err(error) => {
if error.kind() == io::ErrorKind::NotFound {
let default_branch_config = Self::default();
default_branch_config.save(directory, name)?;
default_branch_config
} else {
return Err(PackrinthError::FailedToReadToString {
path_to_read: branch_config_path.display().to_string(),
error_message: error.to_string(),
});
}
}
};
Ok(branch_config)
} else {
Err(PackrinthError::DirectoryExpected {
path_that_should_have_been_dir: branch_dir.display().to_string(),
})
}
}
Err(error) => Err(PackrinthError::BranchDoesNotExist {
branch: name.to_string(),
error_message: error.to_string(),
}),
}
}
pub fn save(&self, directory: &Path, name: &str) -> PackrinthResult<()> {
let branch_config_path = directory.join(name).join(BRANCH_CONFIG_FILE_NAME);
json_to_file(self, branch_config_path)
}
pub fn print_display(&self, name: &str) -> PackrinthResult<()> {
println!("Branch {name}:");
println!(" - Branch version: {}", self.version);
println!(" - Main Minecraft version: {}", self.minecraft_version);
println!(
" - Acceptable Minecraft versions: {}",
self.acceptable_minecraft_versions.join(", ")
);
if let Some(mod_loader) = &self.mod_loader {
println!(" - Main mod loader: {}", mod_loader.pretty_value());
match &self.loader_version {
None => return Err(PackrinthError::MainModLoaderProvidedButNoVersion),
Some(loader_version) => println!(" - Main mod loader version: {loader_version}"),
}
}
println!(
" - Acceptable loaders: {}",
Loader::pretty_value_vec(&self.acceptable_loaders).join(", ")
);
if self.manual_files.is_empty() {
println!(" - No manual files are added");
} else {
println!(" - Has manual files added, see the configuration file");
}
Ok(())
}
}
impl Default for BranchConfig {
fn default() -> Self {
Self {
version: "1.0.0-fabric".to_string(),
minecraft_version: "1.21.8".to_string(),
acceptable_minecraft_versions: vec!["1.21.6".to_string(), "1.21.7".to_string()],
mod_loader: Some(MainLoader::Fabric),
loader_version: Some("0.17.2".to_string()),
acceptable_loaders: vec![Loader::Minecraft, Loader::VanillaShader],
manual_files: vec![],
}
}
}
impl BranchFiles {
pub fn from_directory(directory: &Path, name: &str) -> PackrinthResult<Self> {
let branch_dir = directory.join(name);
match fs::metadata(&branch_dir) {
Ok(metadata) => {
if metadata.is_dir() {
let branch_files_path = branch_dir.join(BRANCH_FILES_FILE_NAME);
let branch_files = match fs::read_to_string(&branch_files_path) {
Ok(contents) => {
let branch_files: Self = match serde_json::from_str(&contents) {
Ok(contents) => contents,
Err(error) => {
return Err(PackrinthError::FailedToParseConfigJson {
config_path: branch_files_path.display().to_string(),
error_message: error.to_string(),
});
}
};
branch_files
}
Err(error) => {
if error.kind() == io::ErrorKind::NotFound {
let default_branch_files = Self::default();
default_branch_files.save(directory, name)?;
default_branch_files
} else {
return Err(PackrinthError::FailedToReadToString {
path_to_read: branch_files_path.display().to_string(),
error_message: error.to_string(),
});
}
}
};
Ok(Self {
info: BRANCH_FILES_INFO.to_string(),
projects: branch_files.projects,
files: branch_files.files,
})
} else {
Err(PackrinthError::DirectoryExpected {
path_that_should_have_been_dir: branch_dir.display().to_string(),
})
}
}
Err(error) => Err(PackrinthError::BranchDoesNotExist {
branch: name.to_string(),
error_message: error.to_string(),
}),
}
}
pub fn save(&self, directory: &Path, name: &str) -> PackrinthResult<()> {
let branch_files_path = directory.join(name).join(BRANCH_FILES_FILE_NAME);
json_to_file(self, branch_files_path)
}
}
impl Default for BranchFiles {
fn default() -> Self {
Self {
info: BRANCH_FILES_INFO.to_string(),
projects: vec![],
files: vec![],
}
}
}
impl MainLoader {
#[must_use]
pub const fn pretty_value(&self) -> &str {
match self {
MainLoader::Forge => "Forge",
MainLoader::NeoForge => "NeoForge",
MainLoader::Fabric => "Fabric",
MainLoader::Quilt => "Quilt",
}
}
#[must_use]
pub const fn modrinth_value(&self) -> &str {
match self {
MainLoader::Forge => "forge",
MainLoader::NeoForge => "neoforge",
MainLoader::Fabric => "fabric",
MainLoader::Quilt => "quilt",
}
}
}
impl Loader {
#[must_use]
pub fn pretty_value_vec(loaders: &Vec<Self>) -> Vec<&str> {
let mut values = Vec::new();
for loader in loaders {
values.push(loader.pretty_value());
}
values
}
#[must_use]
pub fn modrinth_value_vec(loaders: &Vec<Self>) -> Vec<&str> {
let mut values = Vec::new();
for loader in loaders {
values.push(loader.modrinth_value());
}
values
}
#[must_use]
pub const fn pretty_value(&self) -> &str {
match self {
Loader::Minecraft => "Minecraft",
Loader::Fabric => "Fabric",
Loader::Forge => "Forge",
Loader::NeoForge => "NeoForge",
Loader::Quilt => "Quilt",
Loader::Babric => "Babric",
Loader::BTABabric => "BTA (Babric)",
Loader::JavaAgent => "Java Agent",
Loader::LegacyFabric => "Legacy Fabric",
Loader::LiteLoader => "LiteLoader",
Loader::RisugamisModLoader => "Risugami's ModLoader",
Loader::NilLoader => "NilLoader",
Loader::Ornithe => "Ornithe",
Loader::Rift => "Rift",
Loader::Canvas => "Canvas",
Loader::Iris => "Iris",
Loader::Optifine => "OptiFine",
Loader::VanillaShader => "Vanilla Shader",
Loader::Bukkit => "Bukkit",
Loader::Folia => "Folia",
Loader::Paper => "Paper",
Loader::Purpur => "Purpur",
Loader::Spigot => "Spigot",
Loader::Sponge => "Sponge",
Loader::BungeeCord => "BungeeCord",
Loader::Velocity => "Velocity",
Loader::Waterfall => "Waterfall",
}
}
#[must_use]
pub const fn modrinth_value(&self) -> &str {
match self {
Loader::Minecraft => "minecraft",
Loader::Fabric => "fabric",
Loader::Forge => "forge",
Loader::NeoForge => "neoforge",
Loader::Quilt => "quilt",
Loader::Babric => "babric",
Loader::BTABabric => "bta-babric",
Loader::JavaAgent => "java-agent",
Loader::LegacyFabric => "legacy-fabric",
Loader::LiteLoader => "liteloader",
Loader::RisugamisModLoader => "modloader",
Loader::NilLoader => "nilloader",
Loader::Ornithe => "ornithe",
Loader::Rift => "rift",
Loader::Canvas => "canvas",
Loader::Iris => "iris",
Loader::Optifine => "optifine",
Loader::VanillaShader => "vanilla",
Loader::Bukkit => "bukkit",
Loader::Folia => "folia",
Loader::Paper => "paper",
Loader::Purpur => "purpur",
Loader::Spigot => "spigot",
Loader::Sponge => "sponge",
Loader::BungeeCord => "bungeecord",
Loader::Velocity => "velocity",
Loader::Waterfall => "waterfall",
}
}
}