minecraft_assets/api/resource/
location.rs

1use std::{borrow::Cow, fmt};
2
3#[allow(missing_docs)]
4pub const MINECRAFT_NAMESPACE: &str = "minecraft";
5
6use crate::api::{ModelIdentifier, ResourceKind};
7
8/// Represents a Minecraft [resource location].
9///
10/// Resource locations are namespaced identifiers referencing blocks, items,
11/// entity types, recipes, functions, advancements, tags, and various other
12/// objects in vanilla Minecraft.
13///
14/// A valid resource location has a format of `"namespace:path"`. If the
15/// `namespace` portion is left out, then `"minecraft"` is the implied
16/// namespace.
17///
18/// # Borrowing / Ownership
19///
20/// To avoid cloning / [`String`] construction when not necessary, this type can
21/// either borrow or take ownership of the underlying string.
22///
23/// By default, no copying or allocating is done. You must call
24/// [`to_owned()`][Self::to_owned] to get an owned identifier.
25///
26/// [resource location]: <https://minecraft.fandom.com/wiki/Resource_location>
27#[derive(Clone, PartialEq, Eq, Hash)]
28pub struct ResourceLocation<'a> {
29    pub(crate) id: Cow<'a, str>,
30    pub(crate) kind: ResourceKind,
31}
32
33impl<'a> ResourceLocation<'a> {
34    /// Constructs a new [`ResourceLocation`] from the given type and id.
35    ///
36    /// The `id` string will be **borrowed**. You can either use [`to_owned()`]
37    /// to convert the location to an owned representation, or construct on
38    /// directly using [`new_owned()`].
39    ///
40    /// [`to_owned()`]: Self::to_owned
41    /// [`new_owned()`]: Self::new_owned
42    ///
43    /// # Example
44    ///
45    /// ```
46    /// # use minecraft_assets::api::*;
47    /// let location = ResourceLocation::new(ResourceKind::BlockModel, "oak_stairs");
48    /// ```
49    pub fn new(kind: ResourceKind, id: &'a str) -> Self {
50        Self {
51            id: Cow::Borrowed(id),
52            kind,
53        }
54    }
55
56    /// Like [`new()`], but returns a [`ResourceLocation`] that owns its
57    /// internal string.
58    ///
59    /// [`new()`]: Self::new
60    pub fn new_owned(kind: ResourceKind, id: String) -> ResourceLocation<'static> {
61        ResourceLocation {
62            id: Cow::Owned(id),
63            kind,
64        }
65    }
66
67    /// Constructs a new [`ResourceLocation`] referencing the [`BlockStates`] of
68    /// the given block id.
69    ///
70    /// [`BlockStates`]: ResourceKind::BlockStates
71    ///
72    /// # Example
73    ///
74    /// ```
75    /// # use minecraft_assets::api::*;
76    /// let location = ResourceLocation::blockstates("stone");
77    /// let location = ResourceLocation::blockstates("minecraft:dirt");
78    /// ```
79    pub fn blockstates(block_id: &'a str) -> Self {
80        Self::new(ResourceKind::BlockStates, block_id)
81    }
82
83    /// Constructs a new [`ResourceLocation`] referencing the [`BlockModel`] of
84    /// the given block id.
85    ///
86    /// [`BlockModel`]: ResourceKind::BlockModel
87    pub fn block_model(block_id: &'a str) -> Self {
88        Self::new(ResourceKind::BlockModel, block_id)
89    }
90
91    /// Constructs a new [`ResourceLocation`] referencing the [`ItemModel`] of
92    /// the given item id.
93    ///
94    /// [`ItemModel`]: ResourceKind::ItemModel
95    pub fn item_model(item_id: &'a str) -> Self {
96        Self::new(ResourceKind::ItemModel, item_id)
97    }
98
99    /// Constructs a new [`ResourceLocation`] referencing the [`Texture`]
100    /// located at the given path.
101    ///
102    /// [`Texture`]: ResourceKind::Texture
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// # use minecraft_assets::api::*;
108    /// let location = ResourceLocation::texture("block/stone");
109    /// let location = ResourceLocation::texture("item/diamond_hoe");
110    pub fn texture(path: &'a str) -> Self {
111        Self::new(ResourceKind::Texture, path)
112    }
113
114    /// Returns the underlying identifier as a string slice.
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// # use minecraft_assets::api::*;
120    /// let location = ResourceLocation::blockstates("stone");
121    /// assert_eq!(location.as_str(), "stone");
122    ///
123    /// let location = ResourceLocation::blockstates("minecraft:dirt");
124    /// assert_eq!(location.as_str(), "minecraft:dirt");
125    /// ```
126    pub fn as_str(&self) -> &str {
127        &self.id
128    }
129
130    /// Returns whether or not this resource location includes an explicit
131    /// namespace.
132    ///
133    /// # Example
134    ///
135    /// ```
136    /// # use minecraft_assets::api::*;
137    /// let location = ResourceLocation::blockstates("foo:bar");
138    /// assert!(location.has_namespace());
139    ///
140    /// let location = ResourceLocation::blockstates("bar");
141    /// assert!(!location.has_namespace());
142    /// ```
143    pub fn has_namespace(&self) -> bool {
144        self.colon_position().is_some()
145    }
146
147    /// Returns the namespace portion of the resource identifier, or
148    /// `"minecraft"` if it does not have an explicit namespace.
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// # use minecraft_assets::api::*;
154    /// let location = ResourceLocation::blockstates("foo:bar");
155    /// assert_eq!(location.namespace(), "foo");
156    ///
157    /// let location = ResourceLocation::blockstates("bar");
158    /// assert_eq!(location.namespace(), "minecraft");
159    ///
160    /// let location = ResourceLocation::blockstates(":bar");
161    /// assert_eq!(location.namespace(), "");
162    /// ```
163    pub fn namespace(&self) -> &str {
164        self.colon_position()
165            .map(|index| &self.id[..index])
166            .unwrap_or_else(|| MINECRAFT_NAMESPACE)
167    }
168
169    /// Returns the path portion of the resource location.
170    ///
171    /// # Note on Models
172    ///
173    /// For [`BlockModel`] or [`ItemModel`] resources, the name will **not**
174    /// include any leading prefix like `block/` or `item/`. See the
175    /// [`ModelIdentifier`] documentation for more information.
176    ///
177    /// [`BlockModel`]: ResourceKind::BlockModel
178    /// [`ItemModel`]: ResourceKind::ItemModel
179    pub fn path(&self) -> &str {
180        if self.is_model() {
181            ModelIdentifier::model_name(&self.id)
182        } else {
183            &self.id
184        }
185    }
186
187    /// Returns what kind of resource is referenced by this location.
188    pub fn kind(&self) -> ResourceKind {
189        self.kind
190    }
191
192    /// Returns true if the resource location refers to a built-in resource.
193    ///
194    /// If `true`, then there is no corresponding file that contains the
195    /// resource.
196    ///
197    /// # Example
198    ///
199    /// ```
200    /// # use minecraft_assets::api::*;
201    /// let loc = ResourceLocation::item_model("builtin/generated");
202    /// assert!(loc.is_builtin());
203    /// ```
204    pub fn is_builtin(&self) -> bool {
205        if self.is_model() {
206            ModelIdentifier::is_builtin(&self.id)
207        } else {
208            false
209        }
210    }
211
212    /// Returns a new location with a canonical representation (i.e.,
213    /// containing an explicit namespace).
214    ///
215    /// This will involve allocating a new [`String`] if `self` does not already
216    /// contain an explicit namespace.
217    ///
218    /// # Examples
219    ///
220    /// Prepends the default namespace when one is not present:
221    ///
222    /// ```
223    /// # use minecraft_assets::api::*;
224    /// let location = ResourceLocation::blockstates("stone");
225    /// let canonical = location.to_canonical();
226    ///
227    /// assert_eq!(canonical.as_str(), "minecraft:stone");
228    /// ```
229    ///
230    /// Performs a shallow copy when a namespace is already present:
231    ///
232    /// ```
233    /// # use minecraft_assets::api::*;
234    /// let location = ResourceLocation::blockstates("foo:bar");
235    /// let canonical = location.to_canonical();
236    ///
237    /// assert_eq!(canonical.as_str(), "foo:bar");
238    ///
239    /// // Prove that it was a cheap copy.
240    /// assert_eq!(
241    ///     location.as_str().as_ptr() as usize,
242    ///     canonical.as_str().as_ptr() as usize,
243    /// );
244    /// ```
245    pub fn to_canonical(&self) -> ResourceLocation<'a> {
246        if self.has_namespace() {
247            self.clone()
248        } else {
249            let canonical = format!("{}:{}", self.namespace(), self.as_str());
250            Self {
251                id: Cow::Owned(canonical),
252                kind: self.kind,
253            }
254        }
255    }
256
257    /// Returns a new [`ResourceLocation`] that owns the underlying string.
258    ///
259    /// This is useful for, e.g., storing the location in a data structure or
260    /// passing it to another thread.
261    ///
262    /// By default, all `ResourceLocation`s borrow the string they are
263    /// constructed with, so no copying will occur unless you call this
264    /// function.
265    ///
266    /// # Examples
267    ///
268    /// Constructing a location using [`From`] simply borrows the data:
269    ///
270    /// ```compile_fail
271    /// # use minecraft_assets::api::*;
272    /// let string = String::new("my:resource");
273    ///
274    /// let location = ResourceLocation::from(&string);
275    ///
276    /// // Location borrows data from `string`, cannot be sent across threads.
277    /// std::thread::spawn(move || println!("{}", location));
278    /// ```
279    ///
280    /// Calling [`to_owned()`][Self::to_owned] on the location allows it to be
281    /// sent to the thread:
282    ///
283    /// ```
284    /// # use minecraft_assets::api::*;
285    /// let string = "my:resource".to_string();
286    ///
287    /// let location = ResourceLocation::blockstates(&string);
288    /// let location = location.to_owned();
289    ///
290    /// std::thread::spawn(move || println!("{}", location));
291    /// ```
292    pub fn to_owned(&self) -> ResourceLocation<'static> {
293        ResourceLocation {
294            id: Cow::Owned(self.id.clone().into_owned()),
295            kind: self.kind,
296        }
297    }
298
299    pub(crate) fn is_model(&self) -> bool {
300        matches!(
301            self.kind,
302            ResourceKind::BlockModel | ResourceKind::ItemModel
303        )
304    }
305
306    fn colon_position(&self) -> Option<usize> {
307        self.id.chars().position(|c| c == ':')
308    }
309}
310
311impl<'a> AsRef<str> for ResourceLocation<'a> {
312    fn as_ref(&self) -> &str {
313        &self.id
314    }
315}
316
317impl<'a> fmt::Debug for ResourceLocation<'a> {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        let kind = format!("{:?}", self.kind);
320        write!(f, "{}({:?})", kind, &self.id)
321    }
322}
323
324impl<'a> fmt::Display for ResourceLocation<'a> {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        write!(f, "{}", self.to_canonical().as_str())
327    }
328}