use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use anyhow::{anyhow, bail, Context};
use mcvm_core::io::{json_from_file, json_to_file_pretty};
use mcvm_shared::modifications::{ClientType, ServerType};
use mcvm_shared::output::{MCVMOutput, MessageContents};
use mcvm_shared::translate;
use serde::{Deserialize, Serialize};
use mcvm_shared::addon::{Addon, AddonKind};
use mcvm_shared::pkg::{PackageAddonOptionalHashes, PackageID};
use super::paths::Paths;
#[derive(Debug)]
pub struct Lockfile {
contents: LockfileContents,
}
#[derive(Serialize, Deserialize, Default, Debug)]
#[serde(default)]
struct LockfileContents {
packages: HashMap<String, HashMap<String, LockfilePackage>>,
instances: HashMap<String, LockfileInstance>,
created_instances: HashSet<String>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default)]
pub(crate) struct LockfileInstance {
pub(crate) version: String,
pub(crate) game_modification_version: Option<String>,
pub(crate) client_type: ClientType,
pub(crate) server_type: ServerType,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LockfilePackage {
addons: Vec<LockfileAddon>,
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct LockfileAddon {
#[serde(alias = "name")]
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
file_name: Option<String>,
files: Vec<String>,
kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "PackageAddonOptionalHashes::is_empty")]
hashes: PackageAddonOptionalHashes,
}
impl LockfileAddon {
pub fn from_addon(addon: &Addon, paths: Vec<PathBuf>) -> Self {
Self {
id: addon.id.clone(),
file_name: Some(addon.file_name.clone()),
files: paths
.iter()
.map(|x| {
x.to_str()
.expect("Failed to convert addon path to a string")
.to_owned()
})
.collect(),
kind: addon.kind.to_string(),
version: addon.version.clone(),
hashes: addon.hashes.clone(),
}
}
pub fn to_addon(&self, pkg_id: PackageID) -> anyhow::Result<Addon> {
Ok(Addon {
kind: AddonKind::parse_from_str(&self.kind)
.ok_or(anyhow!("Invalid addon kind '{}'", self.kind))?,
id: self.id.clone(),
file_name: self
.file_name
.clone()
.expect("Filename should have been filled in or fixed"),
pkg_id,
version: self.version.clone(),
hashes: self.hashes.clone(),
})
}
pub fn remove(&self) -> anyhow::Result<()> {
for file in self.files.iter() {
let path = PathBuf::from(file);
if path.exists() {
fs::remove_file(path).context("Failed to remove addon")?;
}
}
Ok(())
}
}
impl LockfileContents {
pub fn fix(&mut self) {
for (.., instance) in &mut self.packages {
for (.., package) in instance {
for addon in &mut package.addons {
if addon.file_name.is_none() {
addon.file_name = Some(addon.id.clone())
}
}
}
}
}
}
impl Lockfile {
pub fn open(paths: &Paths) -> anyhow::Result<Self> {
let path = Self::get_path(paths);
let mut contents = if path.exists() {
json_from_file(path).context("Failed to open lockfile")?
} else {
LockfileContents::default()
};
contents.fix();
Ok(Self { contents })
}
pub fn get_path(paths: &Paths) -> PathBuf {
paths.internal.join("lock.json")
}
pub fn finish(&mut self, paths: &Paths) -> anyhow::Result<()> {
json_to_file_pretty(Self::get_path(paths), &self.contents)
.context("Failed to write to lockfile")?;
Ok(())
}
pub(crate) fn get_instance(&self, instance: &str) -> Option<&LockfileInstance> {
self.contents.instances.get(instance)
}
pub fn update_package(
&mut self,
id: &str,
instance: &str,
addons: &[LockfileAddon],
o: &mut impl MCVMOutput,
) -> anyhow::Result<Vec<PathBuf>> {
let mut files_to_remove = Vec::new();
let mut new_files = Vec::new();
if let Some(instance) = self.contents.packages.get_mut(instance) {
if let Some(pkg) = instance.get_mut(id) {
let mut indices = Vec::new();
for (i, current) in pkg.addons.iter().enumerate() {
if !addons.iter().any(|x| x.id == current.id) {
indices.push(i);
files_to_remove.extend(current.files.iter().map(PathBuf::from));
}
}
for i in indices {
pkg.addons.remove(i);
}
for requested in addons {
if let Some(current) = pkg.addons.iter().find(|x| x.id == requested.id) {
files_to_remove.extend(
current
.files
.iter()
.filter(|x| !requested.files.contains(x))
.map(PathBuf::from),
);
new_files.extend(
requested
.files
.iter()
.filter(|x| !current.files.contains(x))
.cloned(),
);
} else {
new_files.extend(requested.files.clone());
};
}
pkg.addons = addons.to_vec();
} else {
instance.insert(
id.to_owned(),
LockfilePackage {
addons: addons.to_vec(),
},
);
new_files.extend(addons.iter().flat_map(|x| x.files.clone()));
}
} else {
self.contents
.packages
.insert(instance.to_owned(), HashMap::new());
self.update_package(id, instance, addons, o)?;
}
for file in &new_files {
if PathBuf::from(file).exists() && !file.contains("mcvm_") {
let allow = o
.prompt_yes_no(
false,
MessageContents::Warning(translate!(
o,
OverwriteAddonFilePrompt,
"file" = file
)),
)
.context("Prompt failed")?;
if !allow {
bail!("File '{file}' would be overwritten by an addon");
}
}
}
Ok(files_to_remove)
}
pub fn remove_unused_packages(
&mut self,
instance: &str,
used_packages: &[PackageID],
) -> anyhow::Result<Vec<PathBuf>> {
if let Some(inst) = self.contents.packages.get_mut(instance) {
let mut pkgs_to_remove = Vec::new();
for (pkg, ..) in inst.iter() {
if !used_packages.contains(&PackageID::from(pkg.clone())) {
pkgs_to_remove.push(pkg.clone());
}
}
let mut files_to_remove = Vec::new();
for pkg_id in pkgs_to_remove {
if let Some(pkg) = inst.remove(&pkg_id) {
for addon in pkg.addons {
files_to_remove.extend(addon.files.iter().map(PathBuf::from));
}
}
}
Ok(files_to_remove)
} else {
Ok(vec![])
}
}
pub fn ensure_instance_created(&mut self, instance: &str, version: &str) {
if !self.contents.instances.contains_key(instance) {
self.contents.instances.insert(
instance.to_string(),
LockfileInstance {
version: version.to_string(),
game_modification_version: None,
client_type: ClientType::Vanilla,
server_type: ServerType::Vanilla,
},
);
}
}
pub fn update_instance_version(&mut self, instance: &str, version: &str) -> anyhow::Result<()> {
if let Some(instance) = self.contents.instances.get_mut(instance) {
instance.version = version.to_string();
Ok(())
} else {
bail!("Instance {instance} does not exist")
}
}
pub fn update_instance_game_modifications(
&mut self,
instance: &str,
client_type: ClientType,
server_type: ServerType,
) -> anyhow::Result<()> {
if let Some(instance) = self.contents.instances.get_mut(instance) {
instance.client_type = client_type;
instance.server_type = server_type;
Ok(())
} else {
bail!("Instance {instance} does not exist")
}
}
pub fn update_instance_game_modification_version(
&mut self,
instance: &str,
version: Option<String>,
) -> anyhow::Result<()> {
if let Some(instance) = self.contents.instances.get_mut(instance) {
instance.game_modification_version = version;
Ok(())
} else {
bail!("Instance {instance} does not exist")
}
}
pub fn has_instance_done_first_update(&self, instance: &str) -> bool {
self.contents.created_instances.contains(instance)
}
pub fn update_instance_has_done_first_update(&mut self, instance: &str) {
self.contents.created_instances.insert(instance.to_string());
}
}