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