Skip to main content

forge/manifest_ledger/
collect.rs

1// #t(file: rust_alloc_in_loop) the offline ledger intentionally owns one stable string per operation.
2
3use 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            // #t(rust_duplicate_strings) Each ledger stage owns explicit placeholder evidence without coupling its private modules.
122            "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}