1use std::fmt::Display;
2use std::path::PathBuf;
3use std::str::FromStr;
4use glacier_ini::ini_file::IniFileError;
5use glacier_ini::IniFileSystem;
6use lazy_regex::{Lazy, Regex, regex};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::encryption::xtea::XteaError;
12use crate::misc::resource_id::ResourceID;
13use crate::resource::pdefs::GameDiscoveryError::InvalidRuntimePath;
14use crate::resource::pdefs::PackageDefinitionSource::{HM2, HM2016, HM3};
15use crate::resource::pdefs::PartitionType::{Dlc, LanguageDlc, LanguageStandard, Standard};
16use crate::resource::resource_partition::PatchId;
17use crate::{utils, WoaVersion};
18
19pub mod h2016_parser;
20pub mod hm2_parser;
21pub mod hm3_parser;
22pub mod bond_parser;
23
24const RESOURCE_PATH_REGEX: &Lazy<Regex> = regex!(r"(\[[A-z]+:/.+?]).([A-z]+)");
25
26#[derive(Debug, Error)]
27pub enum PackageDefinitionError {
28 #[error("Text encoding error: {0}")]
29 TextEncodingError(#[from] std::string::FromUtf8Error),
30
31 #[error("Decryption error: {0}")]
32 DecryptionError(#[from] XteaError),
33
34 #[error("Invalid packagedefintiion file: ({0})")]
35 UnexpectedFormat(String),
36
37 #[error("Failed to read packagedefinition.txt: {0}")]
38 FailedToRead(#[from] std::io::Error),
39}
40
41#[derive(Debug, Error)]
42pub enum PartitionIdError {
43 #[error("couldn't recognize the partition id: {0}")]
44 ParsingError(String),
45
46 #[error("couldn't compile regex: {0}")]
47 RegexError(#[from] regex::Error),
48}
49
50#[derive(Debug, Error)]
51pub enum PartitionInfoError {
52 #[error("couldn't init with partition id: {0}")]
53 IdError(#[from] PartitionIdError),
54}
55
56#[derive(Clone, Debug, PartialEq, Hash, Eq, Default)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub enum PartitionType {
59 #[default]
60 Standard,
61 Addon,
62 Dlc,
63 LanguageStandard(String),
64 LanguageDlc(String),
65}
66
67#[derive(Default, Clone, Debug, PartialEq, Hash, Eq)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69pub struct PartitionId {
70 pub part_type: PartitionType,
71 pub index: usize,
72}
73
74impl PartitionId {
75 pub fn to_filename(&self, patch_index: PatchId) -> String {
76 match patch_index {
77 PatchId::Base => {
78 let base = self.to_string();
79 format!("{base}.rpkg")
80 }
81 PatchId::Patch(patch_idx) => {
82 let base = self.to_string();
83 format!("{base}patch{patch_idx}.rpkg")
84 }
85 }
86 }
87}
88
89impl FromStr for PartitionId {
90 type Err = PartitionIdError;
91
92 fn from_str(id: &str) -> Result<Self, Self::Err> {
93 let regex = regex!("^(chunk|dlc)(\\d+)(\\p{L}*)(?:patch\\d+)?$");
94 if regex.is_match(id) {
95 let matches = regex
96 .captures(id)
97 .ok_or(PartitionIdError::ParsingError(id.to_string()))?;
98 let s: String = matches[1].parse().map_err(|e| {
99 PartitionIdError::ParsingError(format!(
100 "Unable to parse {:?} to a string: {}",
101 &matches[1], e
102 ))
103 })?;
104 let lang: Option<String> = match matches[3].parse::<String>().map_err(|e| {
105 PartitionIdError::ParsingError(format!(
106 "Unable to parse {:?} to a string {}",
107 &matches[3], e
108 ))
109 })? {
110 s if s.is_empty() => None,
111 s => Some(s),
112 };
113
114 let part_type = match s.as_str() {
115 "chunk" => match lang {
116 None => Standard,
117 Some(lang) => LanguageStandard(lang.replace("lang", "")),
118 },
119 "dlc" => match lang {
120 None => Dlc,
121 Some(lang) => LanguageDlc(lang.replace("lang", "")),
122 },
123 _ => Standard,
124 };
125
126 return Ok(Self {
127 part_type,
128 index: matches[2].parse().map_err(|e| {
129 PartitionIdError::ParsingError(format!(
130 "Unable to parse {:?} to a string: {}",
131 &matches[2], e
132 ))
133 })?,
134 });
135 }
136 Err(PartitionIdError::ParsingError(format!(
137 "Unable to parse {id} to a partitionId"
138 )))
139 }
140}
141
142impl Display for PartitionId {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 let str = match &self.part_type {
145 PartitionType::Standard => {
146 format!("chunk{}", self.index)
147 }
148 PartitionType::Addon => {
149 format!("chunk{}", self.index)
150 }
151 PartitionType::Dlc => {
152 format!("dlc{}", self.index)
153 }
154 PartitionType::LanguageStandard(lang) => {
155 format!("chunk{}lang{}", self.index, lang)
156 }
157 PartitionType::LanguageDlc(lang) => {
158 format!("dlc{}lang{}", self.index, lang)
159 }
160 };
161 write!(f, "{str}")
162 }
163}
164
165#[derive(Clone, Debug)]
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168pub struct PartitionInfo {
169 pub name: Option<String>,
171 pub parent: Option<PartitionId>,
173 pub id: PartitionId,
176 pub patch_level: usize,
178 pub roots: Vec<ResourceID>,
180}
181
182impl PartitionInfo {
183 pub fn from_id(id: &str) -> Result<Self, PartitionInfoError> {
184 Ok(Self {
185 name: None,
186 parent: None,
187 id: id.parse().map_err(PartitionInfoError::IdError)?,
188 patch_level: 0,
189 roots: vec![],
190 })
191 }
192
193 pub fn filename(&self, patch_index: PatchId) -> String {
194 self.id.to_filename(patch_index)
195 }
196
197 #[deprecated(since = "1.1.0", note = "you can push to the roots field directly")]
198 pub fn add_root(&mut self, resource_id: ResourceID) {
199 self.roots.push(resource_id);
200 }
201 #[deprecated(since = "1.1.0", note = "prefer direct access through the roots field")]
202 pub fn roots(&self) -> &Vec<ResourceID> {
203 &self.roots
204 }
205
206 #[deprecated(since = "1.1.0", note = "prefer direct access through the name field")]
207 pub fn name(&self) -> &Option<String> {
208 &self.name
209 }
210
211 #[deprecated(
212 since = "1.1.0",
213 note = "prefer direct access through the parent field"
214 )]
215 pub fn parent(&self) -> &Option<PartitionId> {
216 &self.parent
217 }
218
219 #[deprecated(since = "1.1.0", note = "prefer direct access through the id field")]
220 pub fn id(&self) -> PartitionId {
221 self.id.clone()
222 }
223 #[deprecated(
224 since = "1.1.0",
225 note = "prefer direct access through the patch_level field"
226 )]
227 pub fn max_patch_level(&self) -> usize {
228 self.patch_level
229 }
230
231 pub fn set_max_patch_level(&mut self, patch_level: usize) {
232 self.patch_level = patch_level
233 }
234}
235
236pub trait PackageDefinitionParser {
237 fn parse(data: &[u8]) -> Result<Vec<PartitionInfo>, PackageDefinitionError>;
238}
239
240#[derive(Debug)]
241pub enum PackageDefinitionSource {
242 HM3(Vec<u8>),
243 HM2(Vec<u8>),
244 HM2016(Vec<u8>),
245 Custom(Vec<PartitionInfo>),
246}
247
248impl PackageDefinitionSource {
249 pub fn from_version(woa_version: WoaVersion, data: Vec<u8>) -> Self {
250 match woa_version {
251 WoaVersion::HM2016 => HM2016(data),
252 WoaVersion::HM2 => HM2(data),
253 WoaVersion::HM3 => HM3(data),
254 }
255 }
256
257 pub fn from_file(
263 path: PathBuf,
264 game_version: WoaVersion,
265 ) -> Result<Self, PackageDefinitionError> {
266 let package_definition_data =
267 std::fs::read(path.as_path()).map_err(PackageDefinitionError::FailedToRead)?;
268
269 let package_definition = match game_version {
270 WoaVersion::HM2016 => PackageDefinitionSource::HM2016(package_definition_data),
271 WoaVersion::HM2 => PackageDefinitionSource::HM2(package_definition_data),
272 WoaVersion::HM3 => PackageDefinitionSource::HM3(package_definition_data),
273 };
274
275 Ok(package_definition)
276 }
277
278 pub fn read(&self) -> Result<Vec<PartitionInfo>, PackageDefinitionError> {
279 match self {
280 PackageDefinitionSource::Custom(vec) => Ok(vec.clone()),
281 PackageDefinitionSource::HM3(vec) => hm3_parser::HM3Parser::parse(vec),
282 PackageDefinitionSource::HM2(vec) => hm2_parser::HM2Parser::parse(vec),
283 PackageDefinitionSource::HM2016(vec) => h2016_parser::H2016Parser::parse(vec),
284 }
285 }
286}
287
288pub struct GamePaths {
289 pub project_path: PathBuf,
290 pub runtime_path: PathBuf,
291 pub package_definition_path: PathBuf,
292}
293
294#[derive(Debug, Error)]
295pub enum GameDiscoveryError {
296 #[error("No thumbs.dat file found")]
297 NoThumbsFile,
298
299 #[error("No RUNTIME_PATH found in thumbs.dat")]
300 NoRuntimePath,
301
302 #[error("No PROJECT_PATH found in thumbs.dat")]
303 NoProjectPath,
304
305 #[error("The Runtime path cannot be found")]
306 InvalidRuntimePath,
307
308 #[error("Failed to parse the thumbs.dat file: {0}")]
309 FailedToParseThumbsFile(#[from] IniFileError),
310}
311
312impl GamePaths {
313 pub fn from_retail_directory(retail_directory: PathBuf) -> Result<Self, GameDiscoveryError> {
318 let thumbs_path = retail_directory.join("thumbs.dat");
319
320 let thumbs = IniFileSystem::from_path(thumbs_path.as_path())
322 .map_err(GameDiscoveryError::FailedToParseThumbsFile)?;
323
324 let app_options = &thumbs.root()["application"];
325 let project_path = app_options
326 .options()
327 .get("PROJECT_PATH")
328 .ok_or(GameDiscoveryError::NoProjectPath)?;
329 let relative_runtime_path = app_options
330 .options()
331 .get("RUNTIME_PATH")
332 .ok_or(GameDiscoveryError::NoRuntimePath)?;
333 let mut runtime_path = retail_directory
334 .join(project_path.replace("\\", "/"))
335 .join(relative_runtime_path);
336 if !runtime_path.exists() {
337 runtime_path = retail_directory
338 .join(project_path.replace("\\", "/"))
339 .join(utils::uppercase_first_letter(relative_runtime_path));
340 }
341
342 runtime_path = runtime_path
343 .canonicalize()
344 .map_err(|_| InvalidRuntimePath)?;
345 let package_definition_path = runtime_path.join("packagedefinition.txt");
346
347 Ok(Self {
348 project_path: retail_directory.join(project_path),
349 runtime_path,
350 package_definition_path,
351 })
352 }
353}