1use crate::core::error::WorkshopError;
13
14use super::member::{self, Member, rejected};
15use super::table;
16use super::{PathPart, Settings, SettingsNode, suggest};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct SettingsDiagnostic {
25 pub error: WorkshopError,
27 pub suggestion: Option<String>,
33}
34
35pub fn check_emission_diagnostics(settings: &Settings) -> Vec<SettingsDiagnostic> {
39 let mut diagnostics = Vec::new();
40 for child in &settings.children {
41 match child {
42 SettingsNode::Workshop { children, .. } => {
43 check_workshop_children(children, &mut diagnostics)
44 }
45 SettingsNode::Group { name, children, .. } => match name.as_str() {
46 "main" | "lobby" => {
47 for member in children {
48 check_member(member, &[PathPart::Part(name)], &mut diagnostics);
49 }
50 }
51 "gamemodes" => check_modes(children, &mut diagnostics),
52 "heroes" => check_heroes(children, &mut diagnostics),
53 "extensions" => {
54 for member in children {
55 check_member(member, &[PathPart::Part("extensions")], &mut diagnostics);
56 }
57 }
58 _ => check_opaque_children(children, &mut diagnostics),
59 },
60 other => diagnostics.push(rejected(
61 other.span(),
62 "settings block children must be groups".to_string(),
63 None,
64 )),
65 }
66 }
67 diagnostics
68}
69
70pub fn check_emission(settings: &Settings) -> Vec<WorkshopError> {
73 check_emission_diagnostics(settings)
74 .into_iter()
75 .map(|diagnostic| diagnostic.error)
76 .collect()
77}
78
79fn check_workshop_children(children: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
80 for child in children {
81 match child {
82 SettingsNode::Group { children, .. } | SettingsNode::Workshop { children, .. } => {
83 check_workshop_children(children, errors);
84 }
85 SettingsNode::Raw { .. } => {}
86 other => errors.push(rejected(
87 other.span(),
88 "settings.workshop contains a typed builtin setting".to_string(),
89 None,
90 )),
91 }
92 }
93}
94
95fn check_modes(modes: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
96 for mode in modes {
97 let SettingsNode::Group { name, children, .. } = mode else {
98 errors.push(rejected(
99 mode.span(),
100 "mode entries must be groups".to_string(),
101 None,
102 ));
103 continue;
104 };
105 for member in children {
106 if matches!(member, SettingsNode::Bool { name, .. } if name == "enabled") {
109 continue;
110 }
111 check_member(
112 member,
113 &[PathPart::Part("gamemodes"), PathPart::Part(name)],
114 errors,
115 );
116 }
117 }
118}
119
120fn check_heroes(teams: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
121 for team in teams {
122 let SettingsNode::Group { name, children, .. } = team else {
123 errors.push(rejected(
124 team.span(),
125 "team entries must be groups".to_string(),
126 None,
127 ));
128 continue;
129 };
130 if table::team_name(name).is_none() {
131 errors.push(rejected(
132 team.span(),
133 format!("unknown team '{name}'"),
134 suggest::suggest(name, table::team_spellings()),
135 ));
136 continue;
137 }
138 for member in children {
139 match member {
140 SettingsNode::Group { name, children, .. } => {
141 if table::hero_name(name).is_none() {
142 errors.push(rejected(
143 member.span(),
144 format!("unknown hero '{name}'"),
145 suggest::suggest(name, table::hero_spellings()),
146 ));
147 continue;
148 }
149 for inner in children {
150 check_member(
151 inner,
152 &[PathPart::Part("heroes"), PathPart::Team, PathPart::Hero],
153 errors,
154 );
155 }
156 }
157 other => check_member(other, &[PathPart::Part("heroes"), PathPart::Team], errors),
158 }
159 }
160 }
161}
162
163fn check_opaque_children(children: &[SettingsNode], errors: &mut Vec<SettingsDiagnostic>) {
164 for child in children {
165 match child {
166 SettingsNode::Group { children, .. } => check_opaque_children(children, errors),
169 other => check_member(other, &[], errors),
170 }
171 }
172}
173
174fn check_member(node: &SettingsNode, path: &[PathPart<'_>], errors: &mut Vec<SettingsDiagnostic>) {
175 if matches!(node, SettingsNode::Raw { .. }) {
176 return;
177 }
178 let name = node.name();
179 let mut full = path.to_vec();
180 full.push(PathPart::Part(name));
181 match member::lookup(node, &full).and_then(|entry| member::accept(node, entry)) {
182 Ok(Member::List { elements, kind }) => {
183 for element in elements {
184 if let Err(diagnostic) = kind.resolve(element, name) {
185 errors.push(diagnostic);
186 }
187 }
188 }
189 Ok(_) => {}
190 Err(diagnostic) => errors.push(diagnostic),
191 }
192}