Skip to main content

workshop_rs/settings/schema/
operations.rs

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    /// Apply this edit when the targeted source bytes are unchanged.
17    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    /// The canonical path as typed segments: literal keys and the `<team>`
55    /// / `<hero>` template slots a concrete path fills.
56    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    /// Enumerate the accepted members when this setting uses an enum value.
65    /// Boolean settings backed by an enum token (such as `Enabled`) expose
66    /// that token here as well.
67    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    /// Query effective applicability without exposing table deduplication.
123    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    /// Read an existing source-preserving occurrence with its authored value
265    /// and, when source-backed, its effective numeric value.
266    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    /// Update one existing occurrence without rebuilding the surrounding
294    /// settings tree. Unknown and unrelated source structure is untouched.
295    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    /// Build a source-text edit for one existing scalar occurrence.
316    ///
317    /// The caller retains the original source and applies the returned edit
318    /// while its targeted bytes are unchanged. Comments, whitespace, and every
319    /// other byte remain outside the edit range and are therefore preserved
320    /// without assigning them comment/trivia ownership semantics.
321    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}