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::RuntimeResourceID;
13use lazy_regex::regex;
14use std::str::FromStr;
15use thiserror::Error;
16
17#[cfg(feature = "serde")]
18use serde::{Deserialize, Serialize};
19
20static CONSOLE_TAG: &str = "pc";
21
22#[derive(Error, Debug)]
23pub enum ResourceIDError {
24    #[error("Invalid format {}", _0)]
25    InvalidFormat(String),
26}
27
28#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub struct ResourceID {
31    uri: String,
32}
33
34impl FromStr for ResourceID {
35    type Err = ResourceIDError;
36
37    fn from_str(source: &str) -> Result<Self, Self::Err> {
38        let mut uri = source.to_ascii_lowercase();
39        uri.retain(|c| c as u8 > 0x1F);
40        let rid = Self { uri };
41
42        if !rid.is_valid() {
43            return Err(ResourceIDError::InvalidFormat("".to_string()));
44        };
45
46        Ok(Self {
47            uri: rid.uri.replace(format!("{}_", CONSOLE_TAG).as_str(), ""),
48        })
49    }
50}
51
52impl ResourceID {
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Create a derived ResourceID from a existing one. This nests the original ResourceID
58    /// ```
59    /// # use std::str::FromStr;
60    /// # use rpkg_rs::misc::resource_id::ResourceID;
61    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
62    /// # fn main() -> Result<(), ResourceIDError>{
63    ///     let resource_id = ResourceID::from_str("[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx")?;
64    ///     let derived = resource_id.create_derived("dx11", "mate");
65    ///     assert_eq!(derived.resource_path(), "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate");
66    /// #   Ok(())
67    /// # }
68    /// ```
69    pub fn create_derived(&self, parameters: &str, extension: &str) -> ResourceID {
70        let mut derived = format!("[{}]", self.uri);
71        if !parameters.is_empty() {
72            derived += format!("({})", parameters).as_str();
73        }
74        derived += ".";
75        if !extension.is_empty() {
76            derived += extension;
77        }
78
79        ResourceID { uri: derived }
80    }
81
82    /// Create a ResourceID with aspect parameters
83    /// ```
84    /// # use std::str::FromStr;
85    /// # use rpkg_rs::misc::resource_id::ResourceID;     
86    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
87    ///
88    /// # fn main() -> Result<(), ResourceIDError>{
89    ///  
90    ///     let resource_id = ResourceID::from_str("[assembly:/templates/aspectdummy.aspect].pc_entitytype")?;
91    ///     let sub_id_1 = ResourceID::from_str("[assembly:/_pro/effects/geometry/water.prim].pc_entitytype")?;
92    ///     let sub_id_2 = ResourceID::from_str("[modules:/zdisablecameracollisionaspect.class].entitytype")?;
93    ///
94    ///     let aspect = resource_id.create_aspect(vec![&sub_id_1, &sub_id_2]);
95    ///
96    ///     assert_eq!(aspect.resource_path(), "[assembly:/templates/aspectdummy.aspect]([assembly:/_pro/effects/geometry/water.prim].entitytype,[modules:/zdisablecameracollisionaspect.class].entitytype).pc_entitytype");
97    /// #   Ok(())
98    /// # }
99    ///
100    /// ```
101    pub fn create_aspect(&self, ids: Vec<&ResourceID>) -> ResourceID {
102        let mut rid = self.clone();
103        for id in ids {
104            rid.add_parameter(id.uri.as_str());
105        }
106        rid
107    }
108
109    pub fn add_parameter(&mut self, param: &str) {
110        let params = self.parameters();
111        let new_uri = if params.is_empty() {
112            match self.uri.rfind('.') {
113                Some(index) => {
114                    let mut modified_string = self.uri.to_string();
115                    modified_string.insert(index, '(');
116                    modified_string.insert_str(index + 1, param);
117                    modified_string.insert(index + param.len() + 1, ')');
118                    modified_string
119                }
120                None => self.uri.to_string(), // If no dot found, return the original string
121            }
122        } else {
123            match self.uri.rfind(").") {
124                Some(index) => {
125                    let mut modified_string = self.uri.to_string();
126                    modified_string.insert(index, ',');
127                    modified_string.insert_str(index + 1, param);
128                    modified_string
129                }
130                None => self.uri.to_string(), // If no dot found, return the original string
131            }
132        };
133        self.uri = new_uri;
134    }
135
136    /// Get the resource path.
137    /// Will append the platform tag
138    pub fn resource_path(&self) -> String {
139        let mut platform_uri = String::new();
140
141        if let Some(dot) = self.uri.rfind('.') {
142            platform_uri.push_str(&self.uri[..=dot]);
143            platform_uri.push_str("pc_");
144            platform_uri.push_str(&self.uri[dot + 1..]);
145            platform_uri
146        } else {
147            self.uri.clone()
148        }
149    }
150
151    /// Get the base ResourceID within a derived ResourceID
152    /// ```
153    /// # use std::str::FromStr;
154    /// # use rpkg_rs::misc::resource_id::ResourceID;
155    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
156    /// # fn main() -> Result<(), ResourceIDError>{
157    ///     let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
158    ///     let inner_most_path = resource_id.inner_most_resource_path();
159    ///     assert_eq!(inner_most_path.resource_path(), "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx");
160    /// #    Ok(())
161    /// # }
162    /// ```
163    pub fn inner_most_resource_path(&self) -> ResourceID {
164        let open_count = self.uri.chars().filter(|c| *c == '[').count();
165        if open_count == 1 {
166            return self.clone();
167        }
168
169        let parts = self.uri.splitn(open_count + 1, ']').collect::<Vec<&str>>();
170        let rid_str = format!("{}]{}", parts[0], parts[1])
171            .chars()
172            .skip(open_count - 1)
173            .collect::<String>();
174
175        match Self::from_str(rid_str.as_str()) {
176            Ok(r) => r,
177            Err(_) => self.clone(),
178        }
179    }
180
181    /// Get the base ResourceID within a derived ResourceID
182    /// ```
183    /// # use std::str::FromStr;
184    /// # use rpkg_rs::misc::resource_id::ResourceID;
185    /// # use rpkg_rs::misc::resource_id::ResourceIDError;
186    /// # fn main() -> Result<(), ResourceIDError>{
187    ///  
188    ///     let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
189    ///     let inner_path = resource_id.inner_resource_path();
190    ///
191    ///     assert_eq!(inner_path.resource_path(), "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate");
192    /// #   Ok(())
193    /// }
194    ///
195    /// ```
196    pub fn inner_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 re = regex!(r"\[(.*?)][^]]*$");
203        if let Some(captures) = re.captures(&self.uri) {
204            if let Some(inner_string) = captures.get(1) {
205                if let Ok(rid) = ResourceID::from_str(inner_string.as_str()) {
206                    return rid;
207                }
208            }
209        }
210        self.clone()
211    }
212
213    pub fn protocol(&self) -> Option<String> {
214        match self.uri.find(':') {
215            Some(n) => {
216                let protocol: String = self.uri.chars().take(n).collect();
217                Some(protocol.replace('[', ""))
218            }
219            None => None,
220        }
221    }
222
223    pub fn parameters(&self) -> Vec<String> {
224        let re = regex!(r"(.*)\((.*)\)\.(.*)");
225        if let Some(captures) = re.captures(self.uri.as_str()) {
226            if let Some(cap) = captures.get(2) {
227                return cap
228                    .as_str()
229                    .split(',')
230                    .map(|s: &str| s.to_string())
231                    .collect();
232            }
233        }
234        vec![]
235    }
236
237    pub fn path(&self) -> Option<String> {
238        let path: String = self.uri.chars().skip(1).collect();
239        if let Some(n) = path.rfind('/') {
240            let p: String = path.chars().take(n).collect();
241            if !p.contains('.') {
242                return Some(p);
243            }
244        }
245        None
246    }
247
248    pub fn is_empty(&self) -> bool {
249        self.uri.is_empty()
250    }
251
252    pub fn is_valid(&self) -> bool {
253        {
254            self.uri.starts_with('[')
255                && !self.uri.contains("unknown")
256                && !self.uri.contains('*')
257                && self.uri.contains(']')
258        }
259    }
260
261    pub fn into_rrid(self) -> RuntimeResourceID {
262        RuntimeResourceID::from_resource_id(&self)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    #[test]
270    fn test_parameters() -> Result<(), ResourceIDError> {
271        let mut resource_id = ResourceID::from_str(
272            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx",
273        )?;
274        resource_id.add_parameter("lmao");
275        assert_eq!(resource_id.resource_path(), "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao).pc_fx");
276        assert_eq!(resource_id.parameters(), ["lmao".to_string()]);
277
278        resource_id.add_parameter("lmao2");
279        assert_eq!(resource_id.resource_path(), "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass](lmao,lmao2).pc_fx");
280        Ok(())
281    }
282
283    #[test]
284    fn test_get_inner_most_resource_path() -> Result<(), ResourceIDError> {
285        let resource_id = ResourceID::from_str(
286            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx",
287        )?;
288        let inner_path = resource_id.inner_most_resource_path();
289        assert_eq!(
290            inner_path.resource_path(),
291            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
292        );
293
294        let resource_id = ResourceID::from_str("[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate")?;
295        let inner_path = resource_id.inner_most_resource_path();
296        assert_eq!(
297            inner_path.resource_path(),
298            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
299        );
300
301        let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
302        let inner_path = resource_id.inner_most_resource_path();
303        assert_eq!(
304            inner_path.resource_path(),
305            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
306        );
307
308        Ok(())
309    }
310
311    #[test]
312    fn text_get_inner_resource_path() -> Result<(), ResourceIDError> {
313        let resource_id = ResourceID::from_str(
314            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx",
315        )?;
316        let inner_path = resource_id.inner_resource_path();
317        assert_eq!(
318            inner_path.resource_path(),
319            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
320        );
321
322        let resource_id = ResourceID::from_str("[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate")?;
323        let inner_path = resource_id.inner_resource_path();
324        assert_eq!(
325            inner_path.resource_path(),
326            "[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].pc_fx"
327        );
328
329        let resource_id = ResourceID::from_str("[[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).mate](dx12).pc_mate")?;
330        let inner_path = resource_id.inner_resource_path();
331        assert_eq!(inner_path.resource_path(), "[[assembly:/_pro/_test/usern/materialclasses/ball_of_water_b.materialclass].fx](dx11).pc_mate");
332        Ok(())
333    }
334}