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