rpkg_rs/resource/pdefs/
mod.rs

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/// Represents information about a resource partition.
164#[derive(Clone, Debug)]
165#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
166pub struct PartitionInfo {
167    /// The name of the partition, if available.
168    pub name: Option<String>,
169    /// The parent partition's identifier, if any.
170    pub parent: Option<PartitionId>,
171    /// The identifier of the partition.
172    /// Example: "chunk9", "dlc12" or "dlc5langjp"
173    pub id: PartitionId,
174    /// The patch level of the partition. Note: This is used an an upper bound, any patch above this level will be ignored.
175    pub patch_level: usize,
176    /// The list of resource IDs associated with this partition.
177    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    /// Parses a packagedefinition.txt file.
256    ///
257    /// # Arguments
258    /// - `path` - The path to the packagedefinition.txt file.
259    /// - `game_version` - The version of the game.
260    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    /// Tries to discover the game's paths given its retail directory.
312    ///
313    /// # Arguments
314    /// - `retail_directory` - The path to the game's retail directory.
315    pub fn from_retail_directory(retail_directory: PathBuf) -> Result<Self, GameDiscoveryError> {
316        let thumbs_path = retail_directory.join("thumbs.dat");
317
318        // Parse the thumbs file, so we can find the runtime path.
319        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}