1use 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 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 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(), }
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(), }
146 };
147 self.uri = new_uri;
148 }
149
150 #[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 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 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") && !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}