1use wowlab_fs::path::Path;
4use wowlab_manifest_schema::{ItemManifestEntry, Manifest};
5
6use super::types::{LedgerDisposition, LedgerRow, OperationCategory};
7
8mod aura;
9mod relations;
10mod rows;
11mod spell;
12
13use aura::collect_aura;
14use relations::{collect_manifest_relations, event_effect};
15use rows::{base_row, damage_row, effect_coordinate, hook_placeholder, scalar_option, scalar_row};
16use spell::{collect_auto_attacks, collect_spell};
17
18#[cfg(test)]
19mod tests;
20
21const SPEC_ROW_CAPACITY_PADDING: usize = 8;
22
23pub(super) const EXPLICIT_AURA_EDGE_EVIDENCE: &str = "explicit spell-to-aura edge";
24
25pub(super) fn spec_rows(slug: &str, path: &Path, manifest: &Manifest) -> Vec<LedgerRow> {
26 let path = path.display().to_string();
27 let mut rows = Vec::with_capacity(
28 manifest.auras.len() + manifest.spells.len() + SPEC_ROW_CAPACITY_PADDING,
29 );
30
31 collect_spec_section(&mut rows, slug, &path, manifest);
32 collect_mastery(&mut rows, slug, &path, manifest);
33 collect_resources(&mut rows, slug, &path, manifest);
34
35 for (name, aura) in &manifest.auras {
36 collect_aura(&mut rows, slug, &path, name, aura);
37 }
38
39 for (name, spell) in &manifest.spells {
40 collect_spell(&mut rows, slug, &path, name, spell, &manifest.auras);
41 }
42
43 collect_auto_attacks(&mut rows, slug, &path, manifest);
44 collect_manifest_relations(&mut rows, slug, &path, manifest);
45
46 rows
47}
48
49pub(super) fn item_rows(name: &str, path: &Path, item: &ItemManifestEntry) -> Vec<LedgerRow> {
50 let path = path.display().to_string();
51 let owner = format!("item:{name}");
52 let mut rows =
53 Vec::with_capacity(item.auras.len() + item.spells.len() + item.effects.len() + 1);
54
55 for (aura_name, aura) in &item.auras {
56 collect_aura(&mut rows, &owner, &path, aura_name, aura);
57 }
58
59 for (spell_name, spell) in &item.spells {
60 collect_spell(&mut rows, &owner, &path, spell_name, spell, &item.auras);
61 }
62
63 for (effect_name, effect) in &item.effects {
64 rows.push(
65 base_row(
66 &owner,
67 &path,
68 format!("items.{name}.effects.{effect_name}"),
69 OperationCategory::OrderedContent,
70 LedgerDisposition::Unresolved,
71 "item hook reads a retained DBC scalar",
72 )
73 .effect(effect.spell_id, effect.effect),
74 );
75 }
76
77 if item.rppm.is_some() {
78 rows.push(base_row(
79 &owner,
80 &path,
81 format!("items.{name}.rppm"),
82 OperationCategory::Proc,
83 LedgerDisposition::DataAbsent,
84 "item proc frequency is authored server/content data",
85 ));
86 }
87
88 if !item.rppm_haste_scales {
89 rows.push(base_row(
90 &owner,
91 &path,
92 format!("items.{name}.rppm_haste_scales"),
93 OperationCategory::Proc,
94 LedgerDisposition::Unresolved,
95 "authored RPPM haste policy requires enchant/proc evidence",
96 ));
97 }
98
99 if item.player_cast_hook.is_some() {
100 rows.push(base_row(
101 &owner,
102 &path,
103 format!("items.{name}.player_cast_hook"),
104 OperationCategory::OrderedContent,
105 LedgerDisposition::Unresolved,
106 "hook body requires operation-level source inventory",
107 ));
108 }
109
110 rows
111}
112
113fn collect_spec_section(rows: &mut Vec<LedgerRow>, slug: &str, path: &str, manifest: &Manifest) {
114 if manifest.spec.custom_handler.is_some() {
115 rows.push(base_row(
116 slug,
117 path,
118 "spec.custom_handler",
119 OperationCategory::OrderedContent,
120 LedgerDisposition::Unresolved,
121 "hook body requires operation-level source inventory",
123 ));
124 }
125
126 for (index, aura) in manifest.spec.precombat_auras.iter().flatten().enumerate() {
127 rows.push(base_row(
128 slug,
129 path,
130 format!("spec.precombat_auras[{index}] ({aura})"),
131 OperationCategory::AuraApply,
132 LedgerDisposition::ContentOrdering,
133 "precombat initialization applies the aura before combat",
134 ));
135 }
136
137 if let Some(aura) = &manifest.spec.stealth_aura {
138 rows.push(base_row(
139 slug,
140 path,
141 format!("spec.stealth_aura ({aura})"),
142 OperationCategory::Gate,
143 LedgerDisposition::GenericGap,
144 "stealth state is supplied by an authored aura binding",
145 ));
146 }
147
148 if manifest.spec.has_pet {
149 rows.push(base_row(
150 slug,
151 path,
152 "spec.has_pet",
153 OperationCategory::ActorOrPet,
154 LedgerDisposition::GenericGap,
155 "pet ownership is authored instead of inferred from retained actor data",
156 ));
157 }
158}
159
160fn collect_mastery(rows: &mut Vec<LedgerRow>, slug: &str, path: &str, manifest: &Manifest) {
161 hook_placeholder(
162 rows,
163 slug,
164 path,
165 "mastery.hook",
166 manifest.mastery.spell_id,
167 OperationCategory::Mastery,
168 );
169
170 if manifest.mastery.crit_damage_hook.is_some() {
171 hook_placeholder(
172 rows,
173 slug,
174 path,
175 "mastery.crit_damage_hook",
176 manifest.mastery.spell_id,
177 OperationCategory::Mastery,
178 );
179 }
180}
181
182fn collect_resources(rows: &mut Vec<LedgerRow>, slug: &str, path: &str, manifest: &Manifest) {
183 if manifest.resource.max.is_some() {
184 rows.push(base_row(
185 slug,
186 path,
187 "resource.max",
188 OperationCategory::Resource,
189 LedgerDisposition::DataAbsent,
190 "authored primary-resource capacity overrides power data",
191 ));
192 }
193
194 if manifest.resource.regen.is_some() {
195 rows.push(base_row(
196 slug,
197 path,
198 "resource.regen",
199 OperationCategory::Resource,
200 LedgerDisposition::DataAbsent,
201 "authored primary-resource regeneration overrides power data",
202 ));
203 }
204
205 if manifest.resource.starts_at.is_some() {
206 rows.push(base_row(
207 slug,
208 path,
209 "resource.starts_at",
210 OperationCategory::Resource,
211 LedgerDisposition::DataAbsent,
212 "authored initial resource overrides power data",
213 ));
214 }
215
216 if manifest
217 .secondary_resource
218 .as_ref()
219 .is_some_and(|resource| resource.max.is_some())
220 {
221 rows.push(base_row(
222 slug,
223 path,
224 "secondary_resource.max",
225 OperationCategory::Resource,
226 LedgerDisposition::DataAbsent,
227 "authored secondary-resource capacity overrides power data",
228 ));
229 }
230}