Skip to main content

rpkg_rs/resource/
runtime_resource_id.rs

1//! Runtime identifier for a Glacier Resource.
2//! Can be derived from a [ResourceID] md5 digest
3
4use crate::misc::resource_id::ResourceID;
5use binrw::binrw;
6use md5::{Digest, Md5};
7use std::fmt;
8use std::fmt::{Debug, Formatter};
9use std::hash::Hash;
10use thiserror::Error;
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "serde")]
15use serde_hex::{SerHex, StrictPfx};
16
17const RRID_ID_MASK: u64 = 0x00FF_FFFF_FFFF_FFFF;
18
19#[derive(Error, Debug)]
20pub enum RuntimeResourceIDError {
21    #[error("{} can't represent a valid runtimeResourceID", _0)]
22    InvalidID(u64),
23
24    #[error("Cannot parse {} to a runtimeResourceID", _0)]
25    ParseError(String),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[repr(u8)]
30pub enum PlatformTag {
31    None = 0x00,
32    Pc = 0x01,
33    Ps5 = 0x02,
34    Scarlett = 0x03, //Xbox series X/S
35    Ounce = 0x04, // Nintendo Switch 2
36}
37
38impl PlatformTag {
39    const fn from_u8(bit: u8) -> Option<PlatformTag> {
40        match bit {
41            0x00 => Some(PlatformTag::None),
42            0x01 => Some(PlatformTag::Pc),
43            0x02 => Some(PlatformTag::Ps5),
44            0x03 => Some(PlatformTag::Scarlett),
45            0x04 => Some(PlatformTag::Ounce),
46            _ => None,
47        }
48    }
49}
50
51/// Represents a runtime resource identifier.
52#[derive(Default, PartialEq, Eq, Hash, Clone, Copy)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54#[binrw]
55#[brw(little)]
56pub struct RuntimeResourceID {
57    #[cfg_attr(feature = "serde", serde(with = "SerHex::<StrictPfx>"))]
58    id: u64,
59}
60
61impl PartialEq<u64> for RuntimeResourceID {
62    fn eq(&self, other: &u64) -> bool {
63        self.id == *other
64    }
65}
66
67impl From<u64> for RuntimeResourceID {
68    fn from(value: u64) -> Self {
69        let mut rrid = RuntimeResourceID { id: value };
70        if !rrid.is_valid() {
71            rrid = RuntimeResourceID::invalid();
72        }
73        rrid
74    }
75}
76
77impl From<RuntimeResourceID> for u64 {
78    fn from(value: RuntimeResourceID) -> Self {
79        value.id
80    }
81}
82
83#[allow(deprecated)]
84impl From<ResourceID> for RuntimeResourceID {
85    fn from(value: ResourceID) -> Self {
86        Self::from_resource_id(&value)
87    }
88}
89
90impl From<&str> for RuntimeResourceID {
91    fn from(_: &str) -> Self {
92        unimplemented!("Implicit conversion from &str to RuntimeResourceID is not allowed, use the from_raw_string function, or convert from a ResourceID.");
93    }
94}
95
96impl RuntimeResourceID {
97    pub fn to_hex_string(&self) -> String {
98        format!("{:016X}", self.id)
99    }
100    pub fn is_valid(&self) -> bool {
101        self.platform().is_some() && self.id & RRID_ID_MASK != RRID_ID_MASK
102    }
103    pub fn invalid() -> Self {
104        Self {
105            id: 0x00FFFFFFFFFFFFFF,
106        }
107    }
108
109    /// Create RuntimeResourceID from ResourceID
110    #[deprecated(
111        since = "1.4.0",
112        note = "from_resource_id() hashes the ResourceID using the PC platform and is not platform-agnostic. \
113        Use from_resource_id_with_platform(..., \"pc\", ...) instead. \
114        In a future release `from_resource_id()` will hash the platform-agnostic ResourceID form by default."
115    )]
116    pub fn from_resource_id(rid: &ResourceID) -> Self {
117        Self::from_raw_string(&rid.resource_path_with_platform("pc"))
118    }
119
120    /// Create a RuntimeResourceID from a ResourceID.
121    ///
122    /// `path_platform` is the platform added to the resource path before hashing. Example: the pc in `[assembly:/...].pc_extension`
123    /// `rrid_platform_tag` is the platform tag encoded into the RuntimeResourceID. Example: the 02 prefix in `0x02ABCDEFABCDEF`
124    ///
125    /// These are not always the same. Hitman resources usually hash with `"pc"` but still use `PlatformTag::None`.
126    pub fn from_resource_id_with_platform(rid: &ResourceID, resource_platform: &str, runtime_platform: PlatformTag) -> Self {
127        Self::from_raw_string(&rid.resource_path_with_platform(resource_platform)).with_platform(runtime_platform)
128    }
129
130    ///prefer [from_resource_id] when possible
131    pub fn from_raw_string(string: &str) -> Self {
132        let digest = Md5::digest(string);
133        let mut hash = 0u64;
134        for i in 1..8 {
135            hash |= u64::from(digest[i]) << (8 * (7 - i));
136        }
137
138        Self { id: hash }
139    }
140
141    /// Create RuntimeResourceID from hexadecimal string
142    /// Also accepts 0x prefixed strings
143    pub fn from_hex_string(hex_string: &str) -> Result<Self, RuntimeResourceIDError> {
144        let hex_string = hex_string
145            .strip_prefix("0x")
146            .or_else(|| hex_string.strip_prefix("0X"))
147            .unwrap_or(hex_string);
148
149        match u64::from_str_radix(hex_string, 16) {
150            Ok(num) => {
151                let rrid = RuntimeResourceID { id: num };
152                if !rrid.is_valid() {
153                    Err(RuntimeResourceIDError::InvalidID(num))
154                } else {
155                    Ok(rrid)
156                }
157            }
158            Err(_) => Err(RuntimeResourceIDError::ParseError(hex_string.to_string())),
159        }
160    }
161
162    pub const fn platform(self) -> Option<PlatformTag> {
163        PlatformTag::from_u8((self.id >> 56) as u8)
164    }
165
166    pub const fn with_platform(self, platform: PlatformTag) -> Self {
167        Self {
168            id: ((platform as u64) << 56) | (self.id & RRID_ID_MASK),
169        }
170    }
171
172    pub fn from_raw_string_with_platform(string: &str, platform: PlatformTag) -> Self {
173        Self::from_raw_string(string).with_platform(platform)
174    }
175}
176
177impl Debug for RuntimeResourceID {
178    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
179        write!(f, "{}", self.to_hex_string())
180    }
181}
182
183impl fmt::Display for RuntimeResourceID {
184    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
185        write!(f, "{}", self.to_hex_string())
186    }
187}
188
189// Test section
190#[cfg(test)]
191mod tests {
192    use std::str::FromStr;
193    // Import the test module
194    use super::*;
195
196    #[test]
197    fn test_rrid_conversions() {
198        assert_eq!(
199            RuntimeResourceID::from(0x00123456789ABCDE),
200            0x00123456789ABCDE
201        );
202        assert_eq!(RuntimeResourceID::invalid(), 0x00FFFFFFFFFFFFFF);
203        assert_eq!(
204            RuntimeResourceID::from_raw_string("hello world"),
205            0x00B63BBBE01EEED0
206        );
207        assert_eq!(
208            RuntimeResourceID::from_hex_string("0x00123456789ABCDE").unwrap(),
209            0x00123456789ABCDE
210        );
211        assert_eq!(
212            RuntimeResourceID::from_hex_string("00123456789ABCDE").unwrap(),
213            0x00123456789ABCDE
214        );
215
216        let rid = ResourceID::from_str("[assembly:/_test/lib.a?/test_image.png].pc_webp").unwrap();
217        assert_eq!(
218            RuntimeResourceID::from_resource_id_with_platform(&rid, "pc", PlatformTag::None),
219            0x00290D5B143172A3
220        );
221        assert_eq!(RuntimeResourceID::from(rid), 0x00290D5B143172A3);
222    }
223
224    #[test]
225    fn platform_extraction_works() {
226        let rrid = RuntimeResourceID::from_raw_string("hello world").with_platform(PlatformTag::Ps5);
227        assert_eq!(rrid, 0x02B63BBBE01EEED0);
228        assert_eq!(rrid.platform(), Some(PlatformTag::Ps5));
229    }
230
231    #[test]
232    fn plain_rrid_has_none_platform() {
233        let rrid = RuntimeResourceID::from_raw_string("hello world");
234        assert_eq!(rrid.platform(), Some(PlatformTag::None));
235        assert!(rrid.is_valid());
236    }
237
238    #[test]
239    fn invalid_rrid_is_invalid() {
240        let rrid = RuntimeResourceID { id: 0x00FFFFFFFFFFFFFF };
241        assert!(!rrid.is_valid());
242    }
243
244    #[test]
245    fn unknown_platform_is_invalid() {
246        let rrid = RuntimeResourceID { id: 0x99B63BBBE01EEED0 };
247        assert!(!rrid.is_valid());
248    }
249
250    #[test]
251    fn with_platform_preserves_id() {
252        let rrid = RuntimeResourceID::from_raw_string("hello world");
253        let tagged = rrid.with_platform(PlatformTag::Ounce);
254        assert_eq!(tagged, 0x04B63BBBE01EEED0);
255    }
256}