Skip to main content

rpkg_rs/misc/
resource_id.rs

1//! Path identifier for a Glacier Resource file.
2//!
3//! ResourceID represents a resource identifier with utility methods for manipulating and extracting information from the identifier.
4//! The identifier is expected to follow a specific format: ` [protocol:path/to/file.extension(parameters).platform_extension] `
5//! The parameter can be optional. A ResourceID can also be nested/derived
6//! ### Examples of valid ResourceID
7//! ```txt
8//! [assembly:/images/sprites/player.jpg](asspritesheet).pc_jpeg
9//! [[assembly:/images/sprites/player.jpg](asspritesheet).pc_jpeg].pc_png
10//! ```
11
12use crate::resource::runtime_resource_id::{PlatformTag, RuntimeResourceID};
13use lazy_regex::regex;
14use std::str::FromStr;
15use thiserror::Error;
16
17#[cfg(feature = "serde")]
18use serde::{Deserialize, Serialize};
19
20#[derive(Error, Debug)]
21pub enum ResourceIDError {
22    #[error("Invalid format {}", _0)]
23    InvalidFormat(String),
24}
25
26#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct ResourceID {
29    uri: String,
30}
31
32impl FromStr for ResourceID {
33    type Err = ResourceIDError;
34
35    fn from_str(source: &str) -> Result<Self, Self::Err> {
36        let mut uri = source.to_ascii_lowercase();
37        uri.retain(|c| c as u8 > 0x1F);
38        let rid = Self { uri: uri.clone() };
39
40        if !rid.is_valid() {
41            return Err(ResourceIDError::InvalidFormat("".to_string()));
42        };
43
44        let agnostic_uri = if let Some(dot) = uri.rfind('.') {
45            let left = &uri[..=dot];
46            let right = &uri[dot + 1..];
47
48            let mut out = String::with_capacity(uri.len());
49            out.push_str(left);
50
51            if let Some(underscore) = right.find('_') {
52                out.push_str(&right[underscore + 1..]);
53            } else {
54                out.push_str(right);
55            }
56
57            out
58        } else {
59            uri
60        };
61
62        Ok(Self { uri: agnostic_uri })
63    }
64}
65
66impl ResourceID {
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Create a derived ResourceID from a existing one. This nests the original ResourceID
72    /// ```
73    /// # use std::str::FromStr;
74    /// # use rpkg_rs::misc::resource_id::ResourceID;
75    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
76    /// # fn main() -> Result<(), ResourceIDError>{
77    ///     let resource_id = ResourceID::from_str("[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx")?;
78    ///     let derived = resource_id.create_derived("dx11", "mate");
79    ///     assert_eq!(derived.resource_path_with_platform("pc"), "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate");
80    /// #   Ok(())
81    /// # }
82    /// ```
83    pub fn create_derived(&self, parameters: &str, extension: &str) -> ResourceID {
84        let mut derived = format!("[{}]", self.uri);
85        if !parameters.is_empty() {
86            derived += format!("({parameters})").as_str();
87        }
88        derived += ".";
89        if !extension.is_empty() {
90            derived += extension;
91        }
92
93        ResourceID { uri: derived }
94    }
95
96    /// Create a ResourceID with aspect parameters
97    /// ```
98    /// # use std::str::FromStr;
99    /// # use rpkg_rs::misc::resource_id::ResourceID;     
100    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
101    ///
102    /// # fn main() -> Result<(), ResourceIDError>{
103    ///  
104    ///     let resource_id = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].pc_entitytype")?;
105    ///     let sub_id_1 = ResourceID::from_str("[assembly:/_pro/effects/geometry/water.prim].pc_entitytype")?;
106    ///     let sub_id_2 = ResourceID::from_str("[modules:/zdisablecameracollisionaspect.class].entitytype")?;
107    ///
108    ///     let aspect = resource_id.create_aspect(vec![&sub_id_1, &sub_id_2]);
109    ///
110    ///     assert_eq!(aspect.resource_path_with_platform("pc"), "[assembly:/templates/aspectdummy.aspect]([assembly:/_pro/effects/geometry/water.prim].entitytype,[modules:/zdisablecameracollisionaspect.class].entitytype).pc_entitytype");
111    /// #   Ok(())
112    /// # }
113    ///
114    /// ```
115    pub fn create_aspect(&self, ids: Vec<&ResourceID>) -> ResourceID {
116        let mut rid = self.clone();
117        for id in ids {
118            rid.add_parameter(id.uri.as_str());
119        }
120        rid
121    }
122
123    pub fn add_parameter(&mut self, param: &str) {
124        let params = self.parameters();
125        let new_uri = if params.is_empty() {
126            match self.uri.rfind('.') {
127                Some(index) => {
128                    let mut modified_string = self.uri.to_string();
129                    modified_string.insert(index, '(');
130                    modified_string.insert_str(index + 1, param);
131                    modified_string.insert(index + param.len() + 1, ')');
132                    modified_string
133                }
134                None => self.uri.to_string(), // If no dot found, return the original string
135            }
136        } else {
137            match self.uri.rfind(").") {
138                Some(index) => {
139                    let mut modified_string = self.uri.to_string();
140                    modified_string.insert(index, ',');
141                    modified_string.insert_str(index + 1, param);
142                    modified_string
143                }
144                None => self.uri.to_string(), // If no dot found, return the original string
145            }
146        };
147        self.uri = new_uri;
148    }
149
150    /// Get the resource path.
151    /// Will append the platform tag
152    #[deprecated(
153        since = "1.4.0",
154        note = "Use resource_path_with_platform(\"pc\") instead \
155        resource_path() is not platform_agnostic and will always append the pc platform tag. \
156        This function will be made platform agnostic in an upcoming release \
157        Use uri() for the platform-agnostic form or resource_path_with_platform(...) for a platform specific form.\
158        "
159    )]
160    pub fn resource_path(&self) -> String {
161        self.resource_path_with_platform("pc")
162    }
163
164    pub fn uri(&self) -> &str {
165        &self.uri
166    }
167
168    pub fn resource_path_with_platform(&self, platform_tag: &str) -> String {
169        let mut platform_uri = String::new();
170
171        if let Some(dot) = self.uri.rfind('.') {
172            platform_uri.push_str(&self.uri[..=dot]);
173            if !platform_tag.is_empty(){
174                platform_uri.push_str(platform_tag);
175                platform_uri.push('_');
176            }
177            platform_uri.push_str(&self.uri[dot + 1..]);
178            platform_uri
179        } else {
180            self.uri.clone()
181        }
182    }
183
184    /// Get the base ResourceID within a derived ResourceID
185    /// ```
186    /// # use std::str::FromStr;
187    /// # use rpkg_rs::misc::resource_id::ResourceID;
188    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
189    /// # fn main() -> Result<(), ResourceIDError>{
190    ///     let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
191    ///     let inner_most_path = resource_id.inner_most_resource_path();
192    ///     assert_eq!(inner_most_path.uri(), "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx");
193    /// #    Ok(())
194    /// # }
195    /// ```
196    pub fn inner_most_resource_path(&self) -> ResourceID {
197        let open_count = self.uri.chars().filter(|c| *c == '[').count();
198        if open_count == 1 {
199            return self.clone();
200        }
201
202        let parts = self.uri.splitn(open_count + 1, ']').collect::<Vec<&str>>();
203        let rid_str = format!("{}]{}", parts[0], parts[1])
204            .chars()
205            .skip(open_count - 1)
206            .collect::<String>();
207
208        match Self::from_str(rid_str.as_str()) {
209            Ok(r) => r,
210            Err(_) => self.clone(),
211        }
212    }
213
214    /// Get the base ResourceID within a derived ResourceID
215    /// ```
216    /// # use std::str::FromStr;
217    /// # use rpkg_rs::misc::resource_id::ResourceID;
218    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
219    /// # fn main() -> Result<(), ResourceIDError>{
220    ///  
221    ///     let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
222    ///     let inner_path = resource_id.inner_resource_path();
223    ///
224    ///     assert_eq!(inner_path.resource_path_with_platform("pc"), "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate");
225    /// #   Ok(())
226    /// }
227    ///
228    /// ```
229    pub fn inner_resource_path(&self) -> ResourceID {
230        let open_count = self.uri.chars().filter(|c| *c == '[').count();
231        if open_count == 1 {
232            return self.clone();
233        }
234
235        let re = regex!(r"\[(.*?)][^]]*$");
236        if let Some(captures) = re.captures(&self.uri) {
237            if let Some(inner_string) = captures.get(1) {
238                if let Ok(rid) = ResourceID::from_str(inner_string.as_str()) {
239                    return rid;
240                }
241            }
242        }
243        self.clone()
244    }
245
246    pub fn protocol(&self) -> Option<String> {
247        match self.uri.find(':') {
248            Some(n) => {
249                let protocol: String = self.uri.chars().take(n).collect();
250                Some(protocol.replace('[', ""))
251            }
252            None => None,
253        }
254    }
255
256    pub fn parameters(&self) -> Vec<String> {
257        let re = regex!(r"(.*)\((.*)\)\.(.*)");
258        if let Some(captures) = re.captures(self.uri.as_str()) {
259            if let Some(cap) = captures.get(2) {
260                return cap
261                    .as_str()
262                    .split(',')
263                    .map(|s: &str| s.to_string())
264                    .collect();
265            }
266        }
267        vec![]
268    }
269
270    pub fn path(&self) -> Option<String> {
271        let path: String = self.uri.chars().skip(1).collect();
272        if let Some(n) = path.rfind('/') {
273            let p: String = path.chars().take(n).collect();
274            if !p.contains('.') {
275                return Some(p);
276            }
277        }
278        None
279    }
280
281    pub fn is_empty(&self) -> bool {
282        self.uri.is_empty()
283    }
284
285    pub fn is_valid(&self) -> bool {
286        {
287            self.uri.starts_with('[')
288                && !self.uri.contains("unknown") //This isn't a good check :/
289                && !self.uri.contains('*')
290                && self.uri.contains(']')
291        }
292    }
293
294    #[deprecated(
295        since = "1.4.0",
296        note = "into_rrid() hashes the ResourceID using PlatformTag::None. \
297        Use into_rrid_with_platform(..., ..., PlatformTag::None) instead. \
298        In a future release `into_rrid()` will require a runtime platform tag."
299    )]
300    pub fn into_rrid(self) -> RuntimeResourceID {
301        RuntimeResourceID::from_resource_id_with_platform(&self, "", PlatformTag::None)
302    }
303
304    pub fn into_rrid_with_platform(self, resource_platform: &str, runtime_platform: PlatformTag) -> RuntimeResourceID {
305        RuntimeResourceID::from_resource_id_with_platform(&self, resource_platform, runtime_platform)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    #[test]
313    fn test_creation() -> Result<(), ResourceIDError> {
314
315        let rid = ResourceID::from_str(
316            "[assembly:/_PRO/Scenes/Missions/thefacility/vr_tutorial_pc_graduation.brick].entitytype",
317        )?;
318        assert_eq!(
319            rid.uri(),
320            "[assembly:/_pro/scenes/missions/thefacility/vr_tutorial_pc_graduation.brick].entitytype"
321        );
322        assert_eq!(
323            rid.resource_path_with_platform("pc"),
324            "[assembly:/_pro/scenes/missions/thefacility/vr_tutorial_pc_graduation.brick].pc_entitytype"
325        );
326
327        let rid = ResourceID::from_str(
328            "[assembly:/_PRO/Scenes/Missions/thefacility/vr_tutorial_pc_graduation.brick].pc_entitytype",
329        )?;
330        assert_eq!(
331            rid.uri(),
332            "[assembly:/_pro/scenes/missions/thefacility/vr_tutorial_pc_graduation.brick].entitytype"
333        );
334        assert_eq!(
335            rid.resource_path_with_platform("pc"),
336            "[assembly:/_pro/scenes/missions/thefacility/vr_tutorial_pc_graduation.brick].pc_entitytype"
337        );
338
339        let rid = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].ps5_entitytype")?;
340        assert_eq!(
341            rid.uri(),
342            "[assembly:/templates/aspectdummy.aspect].entitytype"
343        );
344        assert_eq!(
345            rid.resource_path_with_platform("ps5"),
346            "[assembly:/templates/aspectdummy.aspect].ps5_entitytype"
347        );
348
349        Ok(())
350    }
351
352    #[test]
353    fn test_parameters_and_derived_ids() -> Result<(), ResourceIDError> {
354        let mut resource_id = ResourceID::from_str(
355            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx",
356        )?;
357        assert_eq!(
358            resource_id.uri(),
359            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx"
360        );
361
362        resource_id.add_parameter("lmao");
363        assert_eq!(
364            resource_id.uri(),
365            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao).fx"
366        );
367        assert_eq!(
368            resource_id.resource_path_with_platform("pc"),
369            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao).pc_fx"
370        );
371        assert_eq!(resource_id.parameters(), ["lmao".to_string()]);
372
373        resource_id.add_parameter("lmao2");
374        assert_eq!(
375            resource_id.uri(),
376            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao,lmao2).fx"
377        );
378        assert_eq!(
379            resource_id.resource_path_with_platform("pc"),
380            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao,lmao2).pc_fx"
381        );
382
383        let derived = resource_id.create_derived("dx11", "mate");
384        assert_eq!(
385            derived.uri(),
386            "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao,lmao2).fx](dx11).mate"
387        );
388        assert_eq!(
389            derived.resource_path_with_platform("pc"),
390            "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao,lmao2).fx](dx11).pc_mate"
391        );
392
393        Ok(())
394    }
395
396    #[test]
397    fn test_get_inner_most_resource_path() -> Result<(), ResourceIDError> {
398        let resource_id = ResourceID::from_str(
399            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx",
400        )?;
401        let inner_path = resource_id.inner_most_resource_path();
402        assert_eq!(
403            inner_path.resource_path_with_platform("pc"),
404            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
405        );
406
407        let resource_id = ResourceID::from_str(
408            "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate",
409        )?;
410        let inner_path = resource_id.inner_most_resource_path();
411        assert_eq!(
412            inner_path.resource_path_with_platform("pc"),
413            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
414        );
415
416        let resource_id = ResourceID::from_str(
417            "[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate",
418        )?;
419        let inner_most = resource_id.inner_most_resource_path();
420        let inner = resource_id.inner_resource_path();
421
422        assert_eq!(
423            inner_most.resource_path_with_platform("pc"),
424            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
425        );
426        assert_eq!(
427            inner.resource_path_with_platform("pc"),
428            "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate"
429        );
430        Ok(())
431    }
432
433    #[test]
434    fn test_rrid_generation_is_agnostic_until_platform_is_explicit() -> Result<(), ResourceIDError>
435    {
436        let pc = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].pc_entitytype")?;
437        let ps5 = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].ps5_entitytype")?;
438        let ounce =
439            ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].ounce_entitytype")?;
440        let plain = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].entitytype")?;
441
442        assert_eq!(
443            pc.uri(),
444            "[assembly:/templates/aspectdummy.aspect].entitytype"
445        );
446        assert_eq!(
447            ps5.uri(),
448            "[assembly:/templates/aspectdummy.aspect].entitytype"
449        );
450        assert_eq!(
451            ounce.uri(),
452            "[assembly:/templates/aspectdummy.aspect].entitytype"
453        );
454        assert_eq!(
455            plain.uri(),
456            "[assembly:/templates/aspectdummy.aspect].entitytype"
457        );
458
459        assert_ne!(
460            plain.clone().into_rrid_with_platform("", PlatformTag::Pc),
461            plain.clone().into_rrid_with_platform("", PlatformTag::Ps5)
462        );
463        assert_ne!(
464            plain.clone().into_rrid_with_platform("", PlatformTag::Ps5),
465            plain.clone().into_rrid_with_platform("", PlatformTag::Ounce)
466        );
467
468        Ok(())
469    }
470
471    #[test]
472    fn test_invalid_inputs() {
473        assert!(ResourceID::from_str("not a resource id").is_err());
474        assert!(ResourceID::from_str("unknown").is_err());
475        assert!(ResourceID::from_str("[assembly:/foo/bar].*").is_err());
476    }
477}