1use std::ops::Range;
2
3use super::super::table::{self, KeyKind};
4use super::*;
5use crate::gameplay::{AbilityVariant, GameplayDataError, HeroId, LogicalSlot, data};
6
7impl SettingSourceEdit {
8 pub fn range(&self) -> Range<usize> {
9 self.edit.range()
10 }
11
12 pub fn replacement(&self) -> &str {
13 self.edit.replacement()
14 }
15
16 pub fn apply(&self, source: &str) -> Result<String, SettingOperationError> {
18 self.edit
19 .apply(source)
20 .map_err(|_| SettingOperationError::SourceMismatch)
21 }
22}
23
24impl SettingPresentation {
25 pub fn localized_name(&self, locale: &str) -> Option<&'static str> {
26 if locale.eq_ignore_ascii_case("en-US") {
27 Some(self.english_name)
28 } else {
29 table::localized_name(locale, self.locale_section, self.english_name)
30 }
31 }
32}
33
34impl SettingDefinition {
35 pub fn identity(&self) -> &SettingIdentity {
36 &self.identity
37 }
38
39 pub fn id(&self) -> Option<&SettingId> {
40 match &self.identity {
41 SettingIdentity::Known(id) => Some(id),
42 SettingIdentity::Unknown => None,
43 }
44 }
45
46 pub fn scope(&self) -> SettingScope {
47 self.scope
48 }
49
50 pub fn path(&self) -> &str {
51 &self.path
52 }
53
54 pub(crate) fn path_parts(&self) -> &[PathPart<'static>] {
55 self.path_parts
56 }
57
58 pub fn domain(&self) -> &SettingValueDomain {
59 &self.domain
60 }
61
62 pub fn enum_members(&self) -> impl Iterator<Item = SettingEnumMember> + '_ {
66 self.enum_domain
67 .into_iter()
68 .flat_map(table::enum_members)
69 .map(|member| SettingEnumMember {
70 domain: member.domain,
71 id: member.member,
72 english_name: member.name,
73 })
74 }
75
76 pub fn target_kind(&self) -> SettingTargetKind {
77 match &self.target {
78 TargetPattern::Global => SettingTargetKind::Global,
79 TargetPattern::Mode(_) => SettingTargetKind::Mode,
80 TargetPattern::Team(_) => SettingTargetKind::Team,
81 TargetPattern::TeamAbility { slot, variant, .. } => SettingTargetKind::TeamAbility {
82 slot: slot.clone(),
83 variant: variant.clone(),
84 },
85 TargetPattern::Hero { .. } => SettingTargetKind::Hero,
86 TargetPattern::HeroAbility { slot, variant, .. } => SettingTargetKind::HeroAbility {
87 slot: slot.clone(),
88 variant: variant.clone(),
89 },
90 TargetPattern::Unknown => SettingTargetKind::Unknown,
91 }
92 }
93
94 pub fn presentation(&self) -> &SettingPresentation {
95 &self.presentation
96 }
97
98 pub fn localized_name(
99 &self,
100 locale: &str,
101 target: &SettingTarget,
102 ) -> Result<Option<&'static str>, GameplayDataError> {
103 match target {
104 SettingTarget::Hero { hero, .. } | SettingTarget::HeroAbility { hero, .. } => {
105 if self.applicability(target)? == Applicability::NotApplicable {
106 Ok(None)
107 } else {
108 Ok(table::hero_setting_name(hero.as_str(), self.key, locale)
109 .or_else(|| self.presentation.localized_name(locale)))
110 }
111 }
112 _ => Ok(self.presentation.localized_name(locale)),
113 }
114 }
115
116 pub fn source(&self) -> SettingSource {
117 self.source
118 }
119
120 pub fn applicability(
122 &self,
123 target: &SettingTarget,
124 ) -> Result<Applicability, GameplayDataError> {
125 Ok(match (&self.target, target) {
126 (TargetPattern::Global, SettingTarget::Global) => Applicability::Applicable,
127 (TargetPattern::Mode(expected), SettingTarget::Mode(actual)) => {
128 let applies = match self.inherited_general_key() {
129 Some(key) => {
133 actual == "general"
134 || table::mode_inherited_entry(&[
135 PathPart::Part("gamemodes"),
136 PathPart::Part(actual.as_str()),
137 PathPart::Part(key),
138 ])
139 .is_some()
140 }
141 None => expected
142 .as_deref()
143 .is_none_or(|expected| expected == actual),
144 };
145 if applies {
146 Applicability::Applicable
147 } else {
148 Applicability::NotApplicable
149 }
150 }
151 (TargetPattern::Team(expected), SettingTarget::Team(actual)) => {
152 if expected
153 .as_deref()
154 .is_none_or(|expected| expected == actual.as_str())
155 {
156 Applicability::Applicable
157 } else {
158 Applicability::NotApplicable
159 }
160 }
161 (TargetPattern::Team(expected), SettingTarget::Hero { team, .. }) => {
162 if team_matches(expected.as_deref(), team.as_ref()) {
163 Applicability::Unknown
164 } else {
165 Applicability::NotApplicable
166 }
167 }
168 (
169 TargetPattern::TeamAbility {
170 team,
171 slot,
172 variant: expected_variant,
173 },
174 SettingTarget::TeamAbility {
175 team: actual_team,
176 slot: actual_slot,
177 variant: actual_variant,
178 },
179 ) => {
180 if !team_matches(team.as_deref(), actual_team.as_ref())
181 || slot != actual_slot
182 || expected_variant
183 .as_ref()
184 .is_some_and(|expected| actual_variant.as_ref() != Some(expected))
185 {
186 Applicability::NotApplicable
187 } else {
188 Applicability::Applicable
189 }
190 }
191 (
192 TargetPattern::TeamAbility {
193 team,
194 slot,
195 variant: expected_variant,
196 },
197 SettingTarget::HeroAbility {
198 team: actual_team,
199 hero: actual_hero,
200 slot: actual_slot,
201 variant: actual_variant,
202 },
203 ) => {
204 if !team_matches(team.as_deref(), actual_team.as_ref())
205 || slot != actual_slot
206 || expected_variant
207 .as_ref()
208 .is_some_and(|expected| actual_variant.as_ref() != Some(expected))
209 {
210 Applicability::NotApplicable
211 } else {
212 match hero_ability_exists(actual_hero, actual_slot, actual_variant.as_ref())? {
213 Some(true) => Applicability::Unknown,
214 Some(false) => Applicability::NotApplicable,
215 None => Applicability::Unknown,
216 }
217 }
218 }
219 (
220 TargetPattern::Hero { team, hero },
221 SettingTarget::Hero {
222 team: actual_team,
223 hero: actual_hero,
224 },
225 ) => {
226 if !team_matches(team.as_deref(), actual_team.as_ref())
227 || hero
228 .as_deref()
229 .is_some_and(|expected| expected != actual_hero.as_str())
230 {
231 Applicability::NotApplicable
232 } else {
233 Applicability::Unknown
234 }
235 }
236 (
237 TargetPattern::HeroAbility {
238 team,
239 hero,
240 slot,
241 variant: expected_variant,
242 },
243 SettingTarget::HeroAbility {
244 team: actual_team,
245 hero: actual_hero,
246 slot: actual_slot,
247 ..
248 },
249 ) => {
250 if !team_matches(team.as_deref(), actual_team.as_ref())
251 || hero
252 .as_deref()
253 .is_some_and(|expected| expected != actual_hero.as_str())
254 || slot.as_str() != actual_slot.as_str()
255 || expected_variant
256 .as_ref()
257 .is_some_and(|expected| Some(expected) != target_variant(target))
258 {
259 return Ok(Applicability::NotApplicable);
260 }
261 match hero_ability_exists(actual_hero, actual_slot, target_variant(target))? {
262 None => Applicability::Unknown,
263 Some(false) => Applicability::NotApplicable,
264 Some(true) => Applicability::Unknown,
265 }
266 }
267 (TargetPattern::Unknown, _) => Applicability::Unknown,
268 _ => Applicability::NotApplicable,
269 })
270 }
271
272 pub fn effective_number(&self, authored: f64) -> Option<EffectiveNumber> {
273 self.domain.effective_number(authored)
274 }
275
276 pub fn read(
279 &self,
280 settings: &Settings,
281 target: &SettingTarget,
282 ) -> Result<SettingOccurrence, SettingOperationError> {
283 let id = self.operation_id()?;
284 self.ensure_read_target(target)?;
285 let path = self.concrete_path(target);
286 let node = find_node(&settings.children, &path).ok_or_else(|| {
287 SettingOperationError::NotFound {
288 setting: id.clone(),
289 target: target.clone(),
290 }
291 })?;
292 let authored = value_from_node(node, &self.domain, &id)?;
293 let effective = match authored {
294 SettingValue::Number(value) | SettingValue::Percent(value) => {
295 self.effective_number(value)
296 }
297 _ => None,
298 };
299 Ok(SettingOccurrence {
300 authored,
301 effective,
302 })
303 }
304
305 pub fn write(
308 &self,
309 settings: &mut Settings,
310 target: &SettingTarget,
311 value: SettingValue,
312 ) -> Result<(), SettingOperationError> {
313 let id = self.operation_id()?;
314 self.ensure_write_target(target)?;
315 let path = self.concrete_path(target);
316 let node = find_node_mut(&mut settings.children, &path).ok_or_else(|| {
317 SettingOperationError::NotFound {
318 setting: id.clone(),
319 target: target.clone(),
320 }
321 })?;
322 if !is_typed_leaf(node) {
323 return Err(not_typed_leaf(node, &id));
324 }
325 let span = node.span();
326 validate_value(&self.domain, &id, &value, span)?;
327 let kind = table::lookup(self.path_parts)
328 .expect("settings definition must retain its table entry")
329 .kind;
330 ensure_supported_value(kind, &id, &value, span)?;
331 apply_value(node, &id, value)
332 }
333
334 pub fn source_edit(
341 &self,
342 source: &str,
343 settings: &Settings,
344 locale: &str,
345 target: &SettingTarget,
346 value: SettingValue,
347 ) -> Result<SettingSourceEdit, SettingOperationError> {
348 let id = self.operation_id()?;
349 self.ensure_write_target(target)?;
350 let path = self.concrete_path(target);
351 let node = find_node(&settings.children, &path).ok_or_else(|| {
352 SettingOperationError::NotFound {
353 setting: id.clone(),
354 target: target.clone(),
355 }
356 })?;
357 let span = node.span();
358 let Some(authored) = typed_leaf_value(node, &self.domain) else {
359 return Err(not_typed_leaf(node, &id));
360 };
361 validate_value(&self.domain, &id, &value, span)?;
362 let kind = table::lookup(self.path_parts)
363 .expect("settings definition must retain its table entry")
364 .kind;
365 ensure_supported_value(kind, &id, &value, span)?;
366 if authored.kind() != self.domain.kind() {
367 return Err(SettingOperationError::WrongValueKind {
368 setting: id.clone(),
369 expected: "existing typed value",
370 actual: value.kind(),
371 span,
372 });
373 }
374 let span = span.ok_or_else(|| SettingOperationError::SourceUnavailable {
375 setting: id.clone(),
376 })?;
377 let range = source_value_range(source, span, &id)?;
378 let replacement = source_value_spelling(&self.domain, kind, locale, &id, value)?;
379 let edit = crate::core::source::SourceEdit::from_source(source, range, replacement)
380 .map_err(|_| SettingOperationError::SourceUnavailable {
381 setting: id.clone(),
382 })?;
383 Ok(SettingSourceEdit { edit })
384 }
385
386 fn ensure_read_target(&self, target: &SettingTarget) -> Result<(), SettingOperationError> {
387 let id = self.operation_id()?;
388 match self
389 .applicability(target)
390 .map_err(|error| SettingOperationError::InvalidValue {
391 setting: id.clone(),
392 message: error.to_string(),
393 span: None,
394 })? {
395 Applicability::NotApplicable => Err(SettingOperationError::NotApplicable {
396 setting: id,
397 target: target.clone(),
398 }),
399 Applicability::Applicable | Applicability::Unknown => Ok(()),
400 }
401 }
402
403 fn ensure_write_target(&self, target: &SettingTarget) -> Result<(), SettingOperationError> {
404 let id = self.operation_id()?;
405 match self
406 .applicability(target)
407 .map_err(|error| SettingOperationError::InvalidValue {
408 setting: id.clone(),
409 message: error.to_string(),
410 span: None,
411 })? {
412 Applicability::NotApplicable => Err(SettingOperationError::NotApplicable {
413 setting: id,
414 target: target.clone(),
415 }),
416 Applicability::Unknown => Err(SettingOperationError::ApplicabilityUnknown {
417 setting: id,
418 target: Box::new(target.clone()),
419 }),
420 Applicability::Applicable => Ok(()),
421 }
422 }
423
424 fn operation_id(&self) -> Result<SettingId, SettingOperationError> {
425 self.id()
426 .cloned()
427 .ok_or_else(|| SettingOperationError::InvalidValue {
428 setting: SettingId::new("unknown"),
429 message: "setting has no reviewed canonical identity".to_string(),
430 span: None,
431 })
432 }
433
434 fn inherited_general_key(&self) -> Option<&'static str> {
435 match self.path_parts {
436 [
437 PathPart::Part("gamemodes"),
438 PathPart::Part("general"),
439 PathPart::Part(key),
440 ] => Some(key),
441 _ => None,
442 }
443 }
444
445 fn concrete_path(&self, target: &SettingTarget) -> Vec<String> {
446 let mode = match target {
447 SettingTarget::Mode(mode) => Some(mode.as_str()),
448 _ => None,
449 };
450 self.path_parts
451 .iter()
452 .enumerate()
453 .map(|(index, part)| match part {
454 PathPart::Part("general")
457 if index == 1 && self.inherited_general_key().is_some() && mode.is_some() =>
458 {
459 mode.unwrap().to_string()
460 }
461 PathPart::Part(name) => (*name).to_string(),
462 PathPart::Team => target_team(target),
463 PathPart::Hero => target_hero(target),
464 })
465 .collect()
466 }
467}
468
469fn target_team(target: &SettingTarget) -> String {
470 match target {
471 SettingTarget::Team(team)
472 | SettingTarget::Hero {
473 team: Some(team), ..
474 }
475 | SettingTarget::TeamAbility {
476 team: Some(team), ..
477 }
478 | SettingTarget::HeroAbility {
479 team: Some(team), ..
480 } => team.as_str().to_string(),
481 _ => "allTeams".to_string(),
482 }
483}
484
485fn target_hero(target: &SettingTarget) -> String {
486 match target {
487 SettingTarget::Hero { hero, .. } | SettingTarget::HeroAbility { hero, .. } => {
488 hero.as_str().to_string()
489 }
490 _ => String::new(),
491 }
492}
493
494fn source_value_range(
495 source: &str,
496 span: crate::core::source::Span,
497 setting: &SettingId,
498) -> Result<Range<usize>, SettingOperationError> {
499 let start = crate::core::source::byte_offset(source, span.start).ok_or_else(|| {
500 SettingOperationError::SourceUnavailable {
501 setting: setting.clone(),
502 }
503 })?;
504 let end = crate::core::source::byte_offset(source, span.end).ok_or_else(|| {
505 SettingOperationError::SourceUnavailable {
506 setting: setting.clone(),
507 }
508 })?;
509 let member =
510 source
511 .get(start..end)
512 .ok_or_else(|| SettingOperationError::SourceUnavailable {
513 setting: setting.clone(),
514 })?;
515 let Some(colon) = member.find(':') else {
516 return Err(SettingOperationError::SourceUnavailable {
517 setting: setting.clone(),
518 });
519 };
520 let value_start = start + colon + 1;
521 let leading = source[value_start..end].len()
522 - source[value_start..end]
523 .trim_start_matches(char::is_whitespace)
524 .len();
525 let range = value_start + leading..end;
526 if range.is_empty() || source.get(range.clone()).is_none() {
527 return Err(SettingOperationError::SourceUnavailable {
528 setting: setting.clone(),
529 });
530 }
531 Ok(range)
532}
533
534fn source_value_spelling(
535 domain: &SettingValueDomain,
536 kind: KeyKind,
537 locale: &str,
538 setting: &SettingId,
539 value: SettingValue,
540) -> Result<String, SettingOperationError> {
541 let localized = |section: &str, english: &str| {
542 if locale.eq_ignore_ascii_case("en-US") {
543 Some(english)
544 } else {
545 table::localized_name(locale, section, english)
546 }
547 .map(str::to_string)
548 .ok_or_else(|| SettingOperationError::InvalidValue {
549 setting: setting.clone(),
550 message: format!("missing {section} locale mapping for '{english}' in {locale}"),
551 span: None,
552 })
553 };
554 match (domain, kind, value) {
555 (SettingValueDomain::Boolean, KeyKind::Bool, SettingValue::Boolean(value)) => {
556 localized("tokens", if value { "On" } else { "Off" })
557 }
558 (SettingValueDomain::Boolean, KeyKind::YesNo, SettingValue::Boolean(value)) => {
559 localized("tokens", if value { "Yes" } else { "No" })
560 }
561 (SettingValueDomain::Boolean, KeyKind::BoolEnum(domain), SettingValue::Boolean(true)) => {
562 let english = table::enum_name(domain, "enabled").ok_or_else(|| {
563 SettingOperationError::InvalidValue {
564 setting: setting.clone(),
565 message: format!("unknown enabled member for enum domain '{domain}'"),
566 span: None,
567 }
568 })?;
569 localized("enums", english)
570 }
571 (SettingValueDomain::Boolean, KeyKind::BoolEnum(_), SettingValue::Boolean(false)) => {
572 Err(bool_enum_false_unsupported(setting, None))
573 }
574 (SettingValueDomain::Number(_), KeyKind::Number, SettingValue::Number(value)) => {
575 Ok(crate::format::format_setting_number(value))
576 }
577 (SettingValueDomain::Percent(_), KeyKind::Percent, SettingValue::Percent(value)) => {
578 Ok(format!("{}%", crate::format::format_setting_number(value)))
579 }
580 (SettingValueDomain::String, KeyKind::String, SettingValue::String(value)) => Ok(format!(
581 "\"{}\"",
582 crate::output::emitter::escape_string(&value)
583 )),
584 (SettingValueDomain::Enum { domain }, KeyKind::Enum(_), SettingValue::Enum(member)) => {
585 let english = table::enum_name(domain, &member).ok_or_else(|| {
586 SettingOperationError::InvalidValue {
587 setting: setting.clone(),
588 message: format!("unknown member '{member}' for enum domain '{domain}'"),
589 span: None,
590 }
591 })?;
592 localized("enums", english)
593 }
594 _ => Err(SettingOperationError::SourceUnavailable {
595 setting: setting.clone(),
596 }),
597 }
598}
599
600fn find_node<'a>(children: &'a [SettingsNode], path: &[String]) -> Option<&'a SettingsNode> {
601 let (name, rest) = path.split_first()?;
602 let node = children.iter().find(|node| node.name() == name)?;
603 if rest.is_empty() {
604 Some(node)
605 } else {
606 match node {
607 SettingsNode::Workshop { children, .. }
608 | SettingsNode::Group { children, .. }
609 | SettingsNode::RawGroup { children, .. } => find_node(children, rest),
610 _ => None,
611 }
612 }
613}
614
615fn find_node_mut<'a>(
616 children: &'a mut [SettingsNode],
617 path: &[String],
618) -> Option<&'a mut SettingsNode> {
619 let (name, rest) = path.split_first()?;
620 let node = children.iter_mut().find(|node| node.name() == name)?;
621 if rest.is_empty() {
622 Some(node)
623 } else {
624 match node {
625 SettingsNode::Workshop { children, .. }
626 | SettingsNode::Group { children, .. }
627 | SettingsNode::RawGroup { children, .. } => find_node_mut(children, rest),
628 _ => None,
629 }
630 }
631}
632
633fn validate_value(
634 domain: &SettingValueDomain,
635 id: &SettingId,
636 value: &SettingValue,
637 span: Option<crate::core::source::Span>,
638) -> Result<(), SettingOperationError> {
639 let expected = domain.kind();
640 if value.kind() != expected {
641 return Err(SettingOperationError::WrongValueKind {
642 setting: id.clone(),
643 expected,
644 actual: value.kind(),
645 span,
646 });
647 }
648 match (domain, value) {
649 (
650 SettingValueDomain::Number(_) | SettingValueDomain::Percent(_),
651 SettingValue::Number(value) | SettingValue::Percent(value),
652 ) if !value.is_finite() => Err(SettingOperationError::InvalidValue {
653 setting: id.clone(),
654 message: "numeric settings values must be finite".to_string(),
655 span,
656 }),
657 (SettingValueDomain::Enum { domain }, SettingValue::Enum(member))
658 if table::enum_name(domain, member).is_none() =>
659 {
660 Err(SettingOperationError::InvalidValue {
661 setting: id.clone(),
662 message: format!("unknown member '{member}' for enum domain '{domain}'"),
663 span,
664 })
665 }
666 (SettingValueDomain::HeroList, SettingValue::HeroList(values))
667 if values.iter().any(|value| table::hero_name(value).is_none()) =>
668 {
669 Err(SettingOperationError::InvalidValue {
670 setting: id.clone(),
671 message: "hero list contains an unknown hero".to_string(),
672 span,
673 })
674 }
675 (SettingValueDomain::MapList, SettingValue::MapList(values))
676 if values.iter().any(|value| table::map_name(value).is_none()) =>
677 {
678 Err(SettingOperationError::InvalidValue {
679 setting: id.clone(),
680 message: "map list contains an unknown map".to_string(),
681 span,
682 })
683 }
684 _ => Ok(()),
685 }
686}
687
688fn is_typed_leaf(node: &SettingsNode) -> bool {
689 matches!(
690 node,
691 SettingsNode::Bool { .. }
692 | SettingsNode::Number { .. }
693 | SettingsNode::String { .. }
694 | SettingsNode::Flag { .. }
695 | SettingsNode::List { .. }
696 )
697}
698
699fn not_typed_leaf(node: &SettingsNode, id: &SettingId) -> SettingOperationError {
700 SettingOperationError::InvalidValue {
701 setting: id.clone(),
702 message: "settings occurrence is not a typed leaf".to_string(),
703 span: node.span(),
704 }
705}
706
707fn typed_leaf_value(node: &SettingsNode, domain: &SettingValueDomain) -> Option<SettingValue> {
708 let value = match node {
709 SettingsNode::Bool { value, .. } => SettingValue::Boolean(*value),
710 SettingsNode::Number { value, .. } => match domain {
711 SettingValueDomain::Percent(_) => SettingValue::Percent(*value),
712 _ => SettingValue::Number(*value),
713 },
714 SettingsNode::String { value, .. } => match domain {
715 SettingValueDomain::Enum { .. } => SettingValue::Enum(value.clone()),
716 _ => SettingValue::String(value.clone()),
717 },
718 SettingsNode::Flag { .. } => SettingValue::PresenceOnly,
719 SettingsNode::List { elements, .. } => {
720 let values = elements
721 .iter()
722 .map(|element| element.value.clone())
723 .collect();
724 match domain {
725 SettingValueDomain::HeroList => SettingValue::HeroList(values),
726 _ => SettingValue::MapList(values),
727 }
728 }
729 _ => return None,
730 };
731 Some(value)
732}
733
734fn ensure_supported_value(
740 kind: KeyKind,
741 id: &SettingId,
742 value: &SettingValue,
743 span: Option<crate::core::source::Span>,
744) -> Result<(), SettingOperationError> {
745 if matches!(
746 (kind, value),
747 (KeyKind::BoolEnum(_), SettingValue::Boolean(false))
748 ) {
749 return Err(bool_enum_false_unsupported(id, span));
750 }
751 Ok(())
752}
753
754fn bool_enum_false_unsupported(
755 id: &SettingId,
756 span: Option<crate::core::source::Span>,
757) -> SettingOperationError {
758 SettingOperationError::InvalidValue {
759 setting: id.clone(),
760 message: "false is unsupported by this Workshop boolean-enum setting".to_string(),
761 span,
762 }
763}
764
765fn value_from_node(
766 node: &SettingsNode,
767 domain: &SettingValueDomain,
768 id: &SettingId,
769) -> Result<SettingValue, SettingOperationError> {
770 let value = typed_leaf_value(node, domain).ok_or_else(|| not_typed_leaf(node, id))?;
771 validate_value(domain, id, &value, node.span())?;
772 Ok(value)
773}
774
775fn apply_value(
776 node: &mut SettingsNode,
777 id: &SettingId,
778 value: SettingValue,
779) -> Result<(), SettingOperationError> {
780 match (node, value) {
781 (SettingsNode::Bool { value: current, .. }, SettingValue::Boolean(value)) => {
782 *current = value
783 }
784 (
785 SettingsNode::Number { value: current, .. },
786 SettingValue::Number(value) | SettingValue::Percent(value),
787 ) => *current = value,
788 (
789 SettingsNode::String { value: current, .. },
790 SettingValue::String(value) | SettingValue::Enum(value),
791 ) => *current = value,
792 (
793 SettingsNode::List { elements, span, .. },
794 SettingValue::HeroList(values) | SettingValue::MapList(values),
795 ) => {
796 if elements.len() != values.len() {
797 return Err(SettingOperationError::InvalidValue {
798 setting: id.clone(),
799 message: "source-preserving list edits cannot change list length".to_string(),
800 span: *span,
801 });
802 }
803 elements
804 .iter_mut()
805 .zip(values)
806 .for_each(|(element, value)| element.value = value);
807 }
808 (SettingsNode::Flag { .. }, SettingValue::PresenceOnly) => {}
809 (node, value) => {
810 return Err(SettingOperationError::WrongValueKind {
811 setting: id.clone(),
812 expected: "existing typed value",
813 actual: value.kind(),
814 span: node.span(),
815 });
816 }
817 }
818 Ok(())
819}
820
821fn team_matches(expected: Option<&str>, actual: Option<&TeamId>) -> bool {
822 expected.is_none_or(|expected| actual.is_some_and(|actual| actual.as_str() == expected))
823}
824
825fn target_variant(target: &SettingTarget) -> Option<&AbilityVariant> {
826 match target {
827 SettingTarget::HeroAbility { variant, .. } => variant.as_ref(),
828 _ => None,
829 }
830}
831
832fn hero_ability_exists(
833 hero: &HeroId,
834 slot: &LogicalSlot,
835 variant: Option<&AbilityVariant>,
836) -> Result<Option<bool>, GameplayDataError> {
837 data::builtin_ref().map_err(Clone::clone).map(|catalog| {
838 catalog.hero(hero).map(|hero| match variant {
839 Some(variant) => hero.ability_variant(slot, variant).is_ok(),
840 None => !hero.abilities_in_slot(slot).is_empty(),
841 })
842 })
843}