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