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