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}