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