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