Skip to main content

rpkg_rs/resource/pdefs/
mod.rs

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