wowlab_engine_ports/
content_catalog.rs1use wowlab_types::{game::SpecId, sim::FastSet};
4
5use crate::{DeclaredSpecMetadataError, SpecDescriptor};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct ResolveDependencies {
10 selector_id: u32,
11 spell_ids: &'static [u32],
12}
13
14impl ResolveDependencies {
15 #[must_use]
17 pub const fn new(selector_id: u32, spell_ids: &'static [u32]) -> Self {
18 Self {
19 selector_id,
20 spell_ids,
21 }
22 }
23
24 #[must_use]
26 pub const fn selector_id(self) -> u32 {
27 self.selector_id
28 }
29
30 #[must_use]
32 pub const fn spell_ids(self) -> &'static [u32] {
33 self.spell_ids
34 }
35}
36
37#[derive(Debug)]
39pub struct ContentCatalog {
40 descriptors: &'static [SpecDescriptor],
41 item_dependencies: &'static [ResolveDependencies],
42 expansion_trait_dependencies: &'static [ResolveDependencies],
43}
44
45impl ContentCatalog {
46 #[must_use]
48 pub const fn new(
49 descriptors: &'static [SpecDescriptor],
50 item_dependencies: &'static [ResolveDependencies],
51 expansion_trait_dependencies: &'static [ResolveDependencies],
52 ) -> Self {
53 Self {
54 descriptors,
55 item_dependencies,
56 expansion_trait_dependencies,
57 }
58 }
59
60 #[must_use]
62 pub const fn descriptors(&self) -> &'static [SpecDescriptor] {
63 self.descriptors
64 }
65
66 #[must_use]
68 pub const fn item_dependencies(&self) -> &'static [ResolveDependencies] {
69 self.item_dependencies
70 }
71
72 #[must_use]
74 pub const fn expansion_trait_dependencies(&self) -> &'static [ResolveDependencies] {
75 self.expansion_trait_dependencies
76 }
77
78 pub fn descriptor(
82 &self,
83 spec_id: SpecId,
84 ) -> Result<&'static SpecDescriptor, ContentCatalogError> {
85 self.validate()?;
86
87 self.descriptors
88 .iter()
89 .find(|descriptor| descriptor.spec_id == spec_id)
90 .ok_or_else(|| {
91 ContentCatalogError::new(ContentCatalogErrorKind::SpecNotFound { spec_id })
92 })
93 }
94
95 #[must_use]
97 pub fn item_resolve_ids(&self, item_id: u32) -> &'static [u32] {
98 resolve_ids(self.item_dependencies, item_id)
99 }
100
101 #[must_use]
103 pub fn expansion_trait_resolve_ids(&self, spell_id: u32) -> &'static [u32] {
104 resolve_ids(self.expansion_trait_dependencies, spell_id)
105 }
106
107 pub fn validate(&self) -> Result<(), ContentCatalogError> {
111 if self.descriptors.is_empty() {
112 return Err(ContentCatalogError::new(
113 ContentCatalogErrorKind::EmptyDescriptors,
114 ));
115 }
116
117 let mut specs = FastSet::default();
118
119 for descriptor in self.descriptors {
120 if !specs.insert(descriptor.spec_id) {
121 return Err(ContentCatalogError::new(
122 ContentCatalogErrorKind::DuplicateSpec {
123 spec_id: descriptor.spec_id,
124 },
125 ));
126 }
127
128 descriptor.metadata.validate().map_err(|source| {
129 ContentCatalogError::new(ContentCatalogErrorKind::InvalidSpecMetadata {
130 spec_id: descriptor.spec_id,
131 source,
132 })
133 })?;
134 }
135
136 validate_dependencies("item", self.item_dependencies)?;
137
138 validate_dependencies("expansion trait", self.expansion_trait_dependencies)
139 }
140}
141
142fn resolve_ids(entries: &[ResolveDependencies], selector_id: u32) -> &'static [u32] {
143 entries
144 .iter()
145 .find(|entry| entry.selector_id == selector_id)
146 .map_or(&[], |entry| entry.spell_ids)
147}
148
149fn validate_dependencies(
150 category: &'static str,
151 entries: &[ResolveDependencies],
152) -> Result<(), ContentCatalogError> {
153 let mut selectors = FastSet::default();
154
155 for entry in entries {
156 if entry.selector_id == 0 {
157 return Err(ContentCatalogError::new(
158 ContentCatalogErrorKind::ZeroSelectorId { category },
159 ));
160 }
161
162 if !selectors.insert(entry.selector_id) {
163 return Err(ContentCatalogError::new(
164 ContentCatalogErrorKind::DuplicateSelectorId {
165 category,
166 selector_id: entry.selector_id,
167 },
168 ));
169 }
170
171 if entry.spell_ids.is_empty() {
172 return Err(ContentCatalogError::new(
173 ContentCatalogErrorKind::EmptyResolveIds {
174 category,
175 selector_id: entry.selector_id,
176 },
177 ));
178 }
179
180 if entry.spell_ids.contains(&0) {
181 return Err(ContentCatalogError::new(
182 ContentCatalogErrorKind::ZeroResolveId {
183 category,
184 selector_id: entry.selector_id,
185 },
186 ));
187 }
188 }
189
190 Ok(())
191}
192
193wowlab_engine_macros::define_error! {
194#[derive(Debug)]
196pub struct ContentCatalogError {
197 #[source]
198 kind: ContentCatalogErrorKind,
199}
200
201#[derive(Debug, thiserror::Error)]
202enum ContentCatalogErrorKind {
203 #[error("no engine-content catalog is linked")]
204 MissingCatalog,
205 #[error("multiple engine-content catalogs are linked")]
206 MultipleCatalogs,
207 #[error("engine-content catalog contains no spec descriptors")]
208 EmptyDescriptors,
209 #[error("engine-content catalog contains duplicate descriptor for {spec_id:?}")]
210 DuplicateSpec { spec_id: SpecId },
211 #[error("engine-content catalog does not contain a descriptor for {spec_id:?}")]
212 SpecNotFound { spec_id: SpecId },
213 #[error("invalid metadata for {spec_id:?}: {source}")]
214 InvalidSpecMetadata {
215 spec_id: SpecId,
216 #[source]
217 source: DeclaredSpecMetadataError,
218 },
219 #[error("{category} resolve dependencies contain reserved selector ID 0")]
220 ZeroSelectorId { category: &'static str },
221 #[error("{category} resolve dependencies contain duplicate selector ID {selector_id}")]
222 DuplicateSelectorId {
223 category: &'static str,
224 selector_id: u32,
225 },
226 #[error("{category} selector {selector_id} declares no resolve IDs")]
227 EmptyResolveIds {
228 category: &'static str,
229 selector_id: u32,
230 },
231 #[error("{category} selector {selector_id} contains reserved resolve ID 0")]
232 ZeroResolveId {
233 category: &'static str,
234 selector_id: u32,
235 },
236}
237}
238
239impl ContentCatalogError {
240 fn new(kind: ContentCatalogErrorKind) -> Self {
241 Self { kind }
242 }
243
244 #[must_use]
246 pub const fn missing_spec(&self) -> Option<SpecId> {
247 match &self.kind {
248 ContentCatalogErrorKind::SpecNotFound { spec_id } => Some(*spec_id),
249 _ => None,
250 }
251 }
252}
253
254inventory::collect!(&'static ContentCatalog);
255
256fn find_catalog_in(
257 catalogs: impl IntoIterator<Item = &'static ContentCatalog>,
258) -> Result<&'static ContentCatalog, ContentCatalogError> {
259 let mut catalogs = catalogs.into_iter();
260 let catalog = catalogs
261 .next()
262 .ok_or_else(|| ContentCatalogError::new(ContentCatalogErrorKind::MissingCatalog))?;
263
264 if catalogs.next().is_some() {
265 return Err(ContentCatalogError::new(
266 ContentCatalogErrorKind::MultipleCatalogs,
267 ));
268 }
269
270 catalog.validate()?;
271
272 Ok(catalog)
273}
274
275pub fn content_catalog() -> Result<&'static ContentCatalog, ContentCatalogError> {
279 find_catalog_in(
280 inventory::iter::<&'static ContentCatalog>
281 .into_iter()
282 .copied(),
283 )
284}
285
286#[cfg(test)]
287mod tests;