1use std::fmt::Display;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use lazy_regex::regex;
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::encryption::xtea::XteaError;
11use crate::misc::ini_file_system::{IniFileError, IniFileSystem};
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!("{}.rpkg", base)
77 }
78 PatchId::Patch(patch_idx) => {
79 let base = self.to_string();
80 format!("{}patch{}.rpkg", base, patch_idx)
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 {} to a partitionId",
135 id
136 )))
137 }
138}
139
140impl Display for PartitionId {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let str = match &self.part_type {
143 PartitionType::Standard => {
144 format!("chunk{}", self.index)
145 }
146 PartitionType::Addon => {
147 format!("chunk{}", self.index)
148 }
149 PartitionType::Dlc => {
150 format!("dlc{}", self.index)
151 }
152 PartitionType::LanguageStandard(lang) => {
153 format!("chunk{}lang{}", self.index, lang)
154 }
155 PartitionType::LanguageDlc(lang) => {
156 format!("dlc{}lang{}", self.index, lang)
157 }
158 };
159 write!(f, "{}", str)
160 }
161}
162
163#[derive(Clone, Debug)]
165#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
166pub struct PartitionInfo {
167 pub name: Option<String>,
169 pub parent: Option<PartitionId>,
171 pub id: PartitionId,
174 pub patch_level: usize,
176 pub roots: Vec<ResourceID>,
178}
179
180impl PartitionInfo {
181 pub fn from_id(id: &str) -> Result<Self, PartitionInfoError> {
182 Ok(Self {
183 name: None,
184 parent: None,
185 id: id.parse().map_err(PartitionInfoError::IdError)?,
186 patch_level: 0,
187 roots: vec![],
188 })
189 }
190
191 pub fn filename(&self, patch_index: PatchId) -> String {
192 self.id.to_filename(patch_index)
193 }
194
195 #[deprecated(since = "1.1.0", note = "you can push to the roots field directly")]
196 pub fn add_root(&mut self, resource_id: ResourceID) {
197 self.roots.push(resource_id);
198 }
199 #[deprecated(since = "1.1.0", note = "prefer direct access through the roots field")]
200 pub fn roots(&self) -> &Vec<ResourceID> {
201 &self.roots
202 }
203
204 #[deprecated(since = "1.1.0", note = "prefer direct access through the name field")]
205 pub fn name(&self) -> &Option<String> {
206 &self.name
207 }
208
209 #[deprecated(
210 since = "1.1.0",
211 note = "prefer direct access through the parent field"
212 )]
213 pub fn parent(&self) -> &Option<PartitionId> {
214 &self.parent
215 }
216
217 #[deprecated(since = "1.1.0", note = "prefer direct access through the id field")]
218 pub fn id(&self) -> PartitionId {
219 self.id.clone()
220 }
221 #[deprecated(
222 since = "1.1.0",
223 note = "prefer direct access through the patch_level field"
224 )]
225 pub fn max_patch_level(&self) -> usize {
226 self.patch_level
227 }
228
229 pub fn set_max_patch_level(&mut self, patch_level: usize) {
230 self.patch_level = patch_level
231 }
232}
233
234pub trait PackageDefinitionParser {
235 fn parse(data: &[u8]) -> Result<Vec<PartitionInfo>, PackageDefinitionError>;
236}
237
238#[derive(Debug)]
239pub enum PackageDefinitionSource {
240 HM3(Vec<u8>),
241 HM2(Vec<u8>),
242 HM2016(Vec<u8>),
243 Custom(Vec<PartitionInfo>),
244}
245
246impl PackageDefinitionSource {
247 pub fn from_version(woa_version: WoaVersion, data: Vec<u8>) -> Self {
248 match woa_version {
249 WoaVersion::HM2016 => HM2016(data),
250 WoaVersion::HM2 => HM2(data),
251 WoaVersion::HM3 => HM3(data),
252 }
253 }
254
255 pub fn from_file(
261 path: PathBuf,
262 game_version: WoaVersion,
263 ) -> Result<Self, PackageDefinitionError> {
264 let package_definition_data =
265 std::fs::read(path.as_path()).map_err(PackageDefinitionError::FailedToRead)?;
266
267 let package_definition = match game_version {
268 WoaVersion::HM2016 => PackageDefinitionSource::HM2016(package_definition_data),
269 WoaVersion::HM2 => PackageDefinitionSource::HM2(package_definition_data),
270 WoaVersion::HM3 => PackageDefinitionSource::HM3(package_definition_data),
271 };
272
273 Ok(package_definition)
274 }
275
276 pub fn read(&self) -> Result<Vec<PartitionInfo>, PackageDefinitionError> {
277 match self {
278 PackageDefinitionSource::Custom(vec) => Ok(vec.clone()),
279 PackageDefinitionSource::HM3(vec) => hm3_parser::HM3Parser::parse(vec),
280 PackageDefinitionSource::HM2(vec) => hm2_parser::HM2Parser::parse(vec),
281 PackageDefinitionSource::HM2016(vec) => h2016_parser::H2016Parser::parse(vec),
282 }
283 }
284}
285
286pub struct GamePaths {
287 pub project_path: PathBuf,
288 pub runtime_path: PathBuf,
289 pub package_definition_path: PathBuf,
290}
291
292#[derive(Debug, Error)]
293pub enum GameDiscoveryError {
294 #[error("No thumbs.dat file found")]
295 NoThumbsFile,
296
297 #[error("No RUNTIME_PATH found in thumbs.dat")]
298 NoRuntimePath,
299
300 #[error("No PROJECT_PATH found in thumbs.dat")]
301 NoProjectPath,
302
303 #[error("The Runtime path cannot be found")]
304 InvalidRuntimePath,
305
306 #[error("Failed to parse the thumbs.dat file: {0}")]
307 FailedToParseThumbsFile(#[from] IniFileError),
308}
309
310impl GamePaths {
311 pub fn from_retail_directory(retail_directory: PathBuf) -> Result<Self, GameDiscoveryError> {
316 let thumbs_path = retail_directory.join("thumbs.dat");
317
318 let thumbs = IniFileSystem::from(thumbs_path.as_path())
320 .map_err(GameDiscoveryError::FailedToParseThumbsFile)?;
321
322 let app_options = &thumbs.root()["application"];
323 let project_path = app_options
324 .options()
325 .get("PROJECT_PATH")
326 .ok_or(GameDiscoveryError::NoProjectPath)?;
327 let relative_runtime_path = app_options
328 .options()
329 .get("RUNTIME_PATH")
330 .ok_or(GameDiscoveryError::NoRuntimePath)?;
331 let mut runtime_path = retail_directory
332 .join(project_path.replace("\\", "/"))
333 .join(relative_runtime_path);
334 if !runtime_path.exists() {
335 runtime_path = retail_directory
336 .join(project_path.replace("\\", "/"))
337 .join(utils::uppercase_first_letter(relative_runtime_path));
338 }
339
340 runtime_path = runtime_path
341 .canonicalize()
342 .map_err(|_| InvalidRuntimePath)?;
343 let package_definition_path = runtime_path.join("packagedefinition.txt");
344
345 Ok(Self {
346 project_path: retail_directory.join(project_path),
347 runtime_path,
348 package_definition_path,
349 })
350 }
351}