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