foch 0.0.2

EU4 mod analysis and merge toolkit
Documentation
use crate::model::GamePath;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// The only verified game identity in Foch's product API.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Eu4;

impl Eu4 {
	pub const STEAM_APP_ID: u32 = 236_850;
	pub const KEY: &'static str = "eu4";
	pub const PARADOX_DATA_DIR_NAME: &'static str = "Europa Universalis IV";

	pub fn from_key(value: &str) -> Option<Self> {
		matches!(
			value.trim().to_ascii_lowercase().as_str(),
			"eu4" | "europauniversalis4" | "europa-universalis-4"
		)
		.then_some(Self)
	}

	pub const fn key(&self) -> &'static str {
		Self::KEY
	}

	pub const fn steam_app_ids(&self) -> &'static [u32] {
		&[Self::STEAM_APP_ID]
	}

	pub const fn paradox_data_dir_name(&self) -> Option<&'static str> {
		Some(Self::PARADOX_DATA_DIR_NAME)
	}

	pub const fn loadable_content_roots(&self) -> Option<&'static [&'static str]> {
		Some(&[
			"common",
			"customizable_localization",
			"decisions",
			"dlc",
			"dlc_metadata",
			"events",
			"fonts",
			"gfx",
			"hints",
			"history",
			"interface",
			"localisation",
			"map",
			"missions",
			"music",
			"music_async",
			"pdx_browser",
			"pdx_online_assets",
			"previewer_assets",
			"sfx",
			"sound",
			"tests",
			"tutorial",
			"tweakergui",
		])
	}

	/// Whether the game loads `relative`: a file below one of the
	/// [`loadable_content_roots`](Self::loadable_content_roots). The top-level
	/// directory is matched ignoring ASCII case.
	pub fn is_loadable_content_path(&self, relative: &GamePath) -> bool {
		if relative.parent().is_none() {
			return false;
		}
		let Some(top) = relative.iter().next() else {
			return false;
		};
		self.loadable_content_roots()
			.expect("EU4 loadable roots are static")
			.iter()
			.any(|root| top.eq_ignore_ascii_case(root))
	}
}

impl Serialize for Eu4 {
	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
	where
		S: Serializer,
	{
		serializer.serialize_str(Self::KEY)
	}
}

impl<'de> Deserialize<'de> for Eu4 {
	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
	where
		D: Deserializer<'de>,
	{
		let value = String::deserialize(deserializer)?;
		Self::from_key(&value)
			.ok_or_else(|| serde::de::Error::custom(format!("unsupported game key {value:?}")))
	}
}