1use serde::Deserialize;
12use serde_json::Value;
13use std::sync::OnceLock;
14
15use super::PathPart::{self, Hero, Part, Team};
16
17const LOCALE_DATA: &str = include_str!("data/locales.json");
21
22fn locale_data() -> &'static Value {
23 static DATA: OnceLock<Value> = OnceLock::new();
24 DATA.get_or_init(|| {
25 serde_json::from_str(LOCALE_DATA).expect("generated settings locale data is valid JSON")
26 })
27}
28
29pub(crate) fn localized_name(locale: &str, section: &str, english: &str) -> Option<&'static str> {
35 let data = locale_data();
36 let aliases = data.get(section)?.get(english)?.as_object()?;
37 aliases.get(locale).and_then(Value::as_str).or_else(|| {
38 aliases.iter().find_map(|(known, value)| {
39 known
40 .eq_ignore_ascii_case(locale)
41 .then(|| value.as_str())
42 .flatten()
43 })
44 })
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct LocalizedSettingName {
50 pub section: &'static str,
53 pub english: &'static str,
55 pub locale: &'static str,
57 pub spelling: &'static str,
59}
60
61const LOCALE_SECTIONS: &[&str] = &[
63 "enums",
64 "heroes",
65 "labels",
66 "maps",
67 "modes",
68 "namespaces",
69 "teams",
70 "tokens",
71];
72
73pub fn localized_setting_names() -> &'static [LocalizedSettingName] {
76 static NAMES: OnceLock<Vec<LocalizedSettingName>> = OnceLock::new();
77 NAMES.get_or_init(|| {
78 let data = locale_data();
79 let mut names = Vec::new();
80 for section in LOCALE_SECTIONS {
81 let Some(rows) = data.get(section).and_then(Value::as_object) else {
82 continue;
83 };
84 for (english, row) in rows {
85 let Some(aliases) = row.as_object() else {
86 continue;
87 };
88 for (locale, value) in aliases {
89 let Some(spelling) = value.as_str() else {
90 continue;
91 };
92 names.push(LocalizedSettingName {
93 section,
94 english,
95 locale,
96 spelling,
97 });
98 }
99 }
100 }
101 names
102 })
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub(crate) enum KeyKind {
108 Flag,
110 String,
112 Bool,
114 YesNo,
116 BoolEnum(&'static str),
119 Number,
121 Percent,
123 Enum(&'static str),
126 ListMap,
128 ListHero,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub(crate) struct TableEntry {
135 pub(crate) path: &'static [PathPart<'static>],
136 pub(crate) workshop_name: &'static str,
137 pub(crate) kind: KeyKind,
138}
139
140macro_rules! entry {
141 ($path:expr, $name:expr, $kind:expr) => {
142 TableEntry {
143 path: &$path,
144 workshop_name: $name,
145 kind: $kind,
146 }
147 };
148}
149
150pub(crate) static ENTRIES: &[TableEntry] = &[
162 entry!(
164 [Part("main"), Part("description")],
165 "Description",
166 KeyKind::String
167 ),
168 entry!(
169 [Part("main"), Part("modeName")],
170 "Mode Name",
171 KeyKind::String
172 ),
173 entry!(
175 [Part("lobby"), Part("ffaSlots")],
176 "Max FFA Players",
177 KeyKind::Number
178 ),
179 entry!(
180 [Part("lobby"), Part("mapRotation")],
181 "Map Rotation",
182 KeyKind::Enum("mapRotation")
183 ),
184 entry!(
185 [Part("lobby"), Part("spectatorSlots")],
186 "Max Spectators",
187 KeyKind::Number
188 ),
189 entry!(
190 [Part("lobby"), Part("enableMatchVoiceChat")],
191 "Match Voice Chat",
192 KeyKind::BoolEnum("matchVoiceChat")
193 ),
194 entry!(
195 [Part("lobby"), Part("team1Slots")],
196 "Max Team 1 Players",
197 KeyKind::Number
198 ),
199 entry!(
200 [Part("lobby"), Part("team2Slots")],
201 "Max Team 2 Players",
202 KeyKind::Number
203 ),
204 entry!(
205 [Part("lobby"), Part("returnToLobby")],
206 "Return To Lobby",
207 KeyKind::Enum("returnToLobby")
208 ),
209 entry!(
210 [Part("lobby"), Part("allowPlayersInQueue")],
211 "Allow Players Who Are In Queue",
212 KeyKind::YesNo
213 ),
214 entry!(
215 [Part("lobby"), Part("swapTeamsAfterMatch")],
216 "Swap Teams After Match",
217 KeyKind::YesNo
218 ),
219 entry!(
226 [Part("gamemodes"), Part("assault"), Part("enabled")],
227 "enabled",
228 KeyKind::Bool
229 ),
230 entry!(
231 [Part("gamemodes"), Part("control"), Part("enabled")],
232 "enabled",
233 KeyKind::Bool
234 ),
235 entry!(
236 [Part("gamemodes"), Part("escort"), Part("enabled")],
237 "enabled",
238 KeyKind::Bool
239 ),
240 entry!(
241 [Part("gamemodes"), Part("hybrid"), Part("enabled")],
242 "enabled",
243 KeyKind::Bool
244 ),
245 entry!(
246 [Part("gamemodes"), Part("assault"), Part("enabledMaps")],
247 "enabled maps",
248 KeyKind::ListMap
249 ),
250 entry!(
251 [Part("gamemodes"), Part("control"), Part("enabledMaps")],
252 "enabled maps",
253 KeyKind::ListMap
254 ),
255 entry!(
256 [Part("gamemodes"), Part("escort"), Part("enabledMaps")],
257 "enabled maps",
258 KeyKind::ListMap
259 ),
260 entry!(
261 [Part("gamemodes"), Part("hybrid"), Part("enabledMaps")],
262 "enabled maps",
263 KeyKind::ListMap
264 ),
265 entry!(
266 [Part("gamemodes"), Part("skirmish"), Part("enabledMaps")],
267 "enabled maps",
268 KeyKind::ListMap
269 ),
270 entry!(
271 [Part("gamemodes"), Part("assault"), Part("disabledMaps")],
272 "disabled maps",
273 KeyKind::ListMap
274 ),
275 entry!(
276 [Part("gamemodes"), Part("skirmish"), Part("disabledMaps")],
277 "disabled maps",
278 KeyKind::ListMap
279 ),
280 entry!(
281 [Part("gamemodes"), Part("ffa"), Part("enabledMaps")],
282 "enabled maps",
283 KeyKind::ListMap
284 ),
285 entry!(
286 [Part("gamemodes"), Part("tdm"), Part("enabledMaps")],
287 "enabled maps",
288 KeyKind::ListMap
289 ),
290 entry!(
291 [Part("gamemodes"), Part("assault"), Part("roleLimit")],
292 "Limit Roles",
293 KeyKind::Enum("roleLimit")
294 ),
295 entry!(
296 [Part("gamemodes"), Part("control"), Part("roleLimit")],
297 "Limit Roles",
298 KeyKind::Enum("roleLimit")
299 ),
300 entry!(
301 [Part("gamemodes"), Part("escort"), Part("roleLimit")],
302 "Limit Roles",
303 KeyKind::Enum("roleLimit")
304 ),
305 entry!(
306 [Part("gamemodes"), Part("hybrid"), Part("roleLimit")],
307 "Limit Roles",
308 KeyKind::Enum("roleLimit")
309 ),
310 entry!(
311 [Part("gamemodes"), Part("general"), Part("roleLimit")],
312 "Limit Roles",
313 KeyKind::Enum("roleLimit")
314 ),
315 entry!(
316 [
317 Part("gamemodes"),
318 Part("assault"),
319 Part("enableCompetitiveRules")
320 ],
321 "Competitive Rules",
322 KeyKind::Bool
323 ),
324 entry!(
325 [
326 Part("gamemodes"),
327 Part("control"),
328 Part("enableCompetitiveRules")
329 ],
330 "Competitive Rules",
331 KeyKind::Bool
332 ),
333 entry!(
334 [
335 Part("gamemodes"),
336 Part("escort"),
337 Part("enableCompetitiveRules")
338 ],
339 "Competitive Rules",
340 KeyKind::Bool
341 ),
342 entry!(
343 [
344 Part("gamemodes"),
345 Part("hybrid"),
346 Part("enableCompetitiveRules")
347 ],
348 "Competitive Rules",
349 KeyKind::Bool
350 ),
351 entry!(
353 [
354 Part("gamemodes"),
355 Part("general"),
356 Part("enableCompetitiveRules")
357 ],
358 "Competitive Rules",
359 KeyKind::Bool
360 ),
361 entry!(
362 [Part("gamemodes"), Part("general"), Part("enablePerks")],
363 "Enable Perks",
364 KeyKind::Bool
365 ),
366 entry!(
367 [Part("gamemodes"), Part("general"), Part("heroLimit")],
368 "Hero Limit",
369 KeyKind::Enum("heroLimit")
370 ),
371 entry!(
372 [Part("gamemodes"), Part("general"), Part("respawnTime%")],
373 "Respawn Time Scalar",
374 KeyKind::Percent
375 ),
376 entry!(
377 [
378 Part("gamemodes"),
379 Part("general"),
380 Part("enableHeroSwitching")
381 ],
382 "Allow Hero Switching",
383 KeyKind::Bool
384 ),
385 entry!(
386 [
387 Part("gamemodes"),
388 Part("general"),
389 Part("enableRandomHeroes")
390 ],
391 "Respawn As Random Hero",
392 KeyKind::Bool
393 ),
394 entry!(
395 [
396 Part("gamemodes"),
397 Part("general"),
398 Part("gamemodeStartTrigger")
399 ],
400 "Game Mode Start",
401 KeyKind::Enum("gamemodeStartTrigger")
402 ),
403 entry!(
404 [
405 Part("gamemodes"),
406 Part("assault"),
407 Part("gamemodeStartTrigger")
408 ],
409 "Game Mode Start",
410 KeyKind::Enum("gamemodeStartTrigger")
411 ),
412 entry!(
413 [
414 Part("gamemodes"),
415 Part("assault"),
416 Part("tankPassiveHealthBonus")
417 ],
418 "Tank Role Passive Health Bonus",
419 KeyKind::Enum("tankPassiveHealthBonus")
420 ),
421 entry!(
422 [Part("gamemodes"), Part("general"), Part("spawnHealthPacks")],
423 "Spawn Health Packs",
424 KeyKind::Enum("spawnHealthPacks")
425 ),
426 entry!(
428 [Part("heroes"), Team, Part("enabledHeroes")],
429 "enabled heroes",
430 KeyKind::ListHero
431 ),
432 entry!(
433 [Part("heroes"), Team, Part("disabledHeroes")],
434 "disabled heroes",
435 KeyKind::ListHero
436 ),
437 entry!(
438 [Part("heroes"), Part("general"), Part("disabledHeroes")],
439 "disabled heroes",
440 KeyKind::ListHero
441 ),
442 entry!(
444 [Part("heroes"), Team, Hero, Part("enablePrimaryFire")],
445 "Primary Fire",
446 KeyKind::Bool
447 ),
448 entry!(
449 [Part("heroes"), Team, Hero, Part("enableSecondaryFire")],
450 "Secondary Fire",
451 KeyKind::Bool
452 ),
453 entry!(
454 [Part("heroes"), Team, Hero, Part("enableAbility1")],
455 "Ability 1",
456 KeyKind::Bool
457 ),
458 entry!(
459 [Part("heroes"), Team, Hero, Part("enableAbility2")],
460 "Ability 2",
461 KeyKind::Bool
462 ),
463 entry!(
464 [Part("heroes"), Team, Hero, Part("health%")],
465 "Health",
466 KeyKind::Percent
467 ),
468 entry!(
469 [Part("heroes"), Team, Hero, Part("passiveUltGen%")],
470 "Ultimate Generation - Passive Blizzard",
471 KeyKind::Percent
472 ),
473 entry!(
474 [Part("heroes"), Team, Hero, Part("combatUltGen%")],
475 "Ultimate Generation - Combat Blizzard",
476 KeyKind::Percent
477 ),
478];
479
480#[derive(Debug, Clone, Copy)]
482pub(crate) struct NameMap {
483 pub(crate) key: &'static str,
484 pub(crate) name: &'static str,
485}
486
487const fn named(key: &'static str, name: &'static str) -> NameMap {
488 NameMap { key, name }
489}
490
491pub(crate) static MODE_NAMES: &[NameMap] = &[
494 named("assault", "Assault"),
495 named("control", "Control"),
496 named("escort", "Escort"),
497 named("hybrid", "Hybrid"),
498 named("skirmish", "Skirmish"),
499 named("ffa", "Deathmatch"),
500 named("tdm", "Team Deathmatch"),
501 named("general", "General"),
502];
503
504pub(crate) static MAP_NAMES: &[NameMap] = &[
506 named("workshopIsland", "Workshop Island"),
507 named("kingsRowWinter", "King's Row Winter"),
508];
509
510pub(crate) static HERO_NAMES: &[NameMap] = &[
512 named("anran", "Anran"),
513 named("ana", "Ana"),
514 named("ashe", "Ashe"),
515 named("bastion", "Bastion"),
516 named("baptiste", "Baptiste"),
517 named("brigitte", "Brigitte"),
518 named("cassidy", "Cassidy"),
519 named("dmon", "D.Mon"),
520 named("domina", "Domina"),
521 named("dva", "D.Va"),
522 named("doomfist", "Doomfist"),
523 named("echo", "Echo"),
524 named("emre", "Emre"),
525 named("freja", "Freja"),
526 named("genji", "Genji"),
527 named("hanzo", "Hanzo"),
528 named("moira", "Moira"),
529 named("reinhardt", "Reinhardt"),
530 named("hazard", "Hazard"),
531 named("illari", "Illari"),
532 named("juno", "Juno"),
533 named("jetpackCat", "Jetpack Cat"),
534 named("junkerQueen", "Junker Queen"),
535 named("junkrat", "Junkrat"),
536 named("kiriko", "Kiriko"),
537 named("lucio", "Lúcio"),
538 named("mauga", "Mauga"),
539 named("mercy", "Mercy"),
540 named("mizuki", "Mizuki"),
541 named("orisa", "Orisa"),
542 named("pharah", "Pharah"),
543 named("reaper", "Reaper"),
544 named("roadhog", "Roadhog"),
545 named("shion", "Shion"),
546 named("sierra", "Sierra"),
547 named("sigma", "Sigma"),
548 named("ramattra", "Ramattra"),
549 named("lifeweaver", "Lifeweaver"),
550 named("sojourn", "Sojourn"),
551 named("soldier", "Soldier: 76"),
552 named("sombra", "Sombra"),
553 named("symmetra", "Symmetra"),
554 named("torbjorn", "Torbjörn"),
555 named("tracer", "Tracer"),
556 named("venture", "Venture"),
557 named("widowmaker", "Widowmaker"),
558 named("winston", "Winston"),
559 named("wuyang", "Wuyang"),
560 named("wreckingBall", "Wrecking Ball"),
561 named("zarya", "Zarya"),
562 named("zenyatta", "Zenyatta"),
563 named("mei", "Mei"),
564];
565
566include!("data/generated_map_entries.rs");
567include!("data/generated_hero_entries.rs");
568include!("data/generated_mode_entries.rs");
569
570pub(crate) static TEAM_NAMES: &[NameMap] = &[
572 named("allTeams", "General"),
573 named("team1", "Team 1"),
574 named("team2", "Team 2"),
575];
576
577#[derive(Debug, Clone, Copy)]
579pub(crate) struct EnumMember {
580 pub(crate) domain: &'static str,
581 pub(crate) member: &'static str,
582 pub(crate) name: &'static str,
583}
584
585const fn enum_member(domain: &'static str, member: &'static str, name: &'static str) -> EnumMember {
586 EnumMember {
587 domain,
588 member,
589 name,
590 }
591}
592
593include!("data/generated_entries.rs");
594include!("data/generated_hero_settings.rs");
595
596pub(crate) static ENUM_MEMBERS: &[EnumMember] = &[
601 enum_member("mapRotation", "afterAGame", "After A Game"),
602 enum_member("mapRotation", "afterMirrorMatch", "After A Mirror Match"),
603 enum_member("mapRotation", "paused", "Paused"),
604 enum_member("matchVoiceChat", "enabled", "Enabled"),
605 enum_member("returnToLobby", "never", "Never"),
606 enum_member("returnToLobby", "afterAGame", "After A Game"),
607 enum_member("returnToLobby", "afterMirrorMatch", "After A Mirror Match"),
608 enum_member("gamemodeStartTrigger", "immediately", "Immediately"),
609 enum_member("gamemodeStartTrigger", "manual", "Manual"),
610 enum_member("gamemodeStartTrigger", "allSlotsFilled", "All Slots Filled"),
611 enum_member("spawnHealthPacks", "disabled", "Disabled"),
612 enum_member("spawnHealthPacks", "modeDependent", "Determined By Mode"),
613 enum_member("spawnHealthPacks", "enabled", "Enabled"),
614 enum_member("roleLimit", "2OfEachRolePerTeam", "2 Of Each Role Per Team"),
615 enum_member(
616 "roleLimit",
617 "1Tank2Offense2Support",
618 "1 Tank 2 Offense 2 Support",
619 ),
620 enum_member("roleLimit", "off", "Off"),
621 enum_member("tankPassiveHealthBonus", "alwaysEnabled", "Always Enabled"),
622 enum_member("tankPassiveHealthBonus", "disabled", "Disabled"),
623 enum_member(
624 "tankPassiveHealthBonus",
625 "1Tank2Offense2Support",
626 "1 Tank 2 Offense 2 Support",
627 ),
628 enum_member("heroLimit", "off", "Off"),
629 enum_member("heroLimit", "1PerTeam", "1 Per Team"),
630 enum_member("heroLimit", "2PerTeam", "2 Per Team"),
631 enum_member("heroLimit", "1PerGame", "1 Per Game"),
632 enum_member("heroLimit", "2PerGame", "2 Per Game"),
633];
634
635pub(crate) fn lookup(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
644 exact_entry(path).or_else(|| mode_inherited_entry(path))
645}
646
647fn exact_entry(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
648 entries().find(|entry| {
649 entry.path.len() == path.len() && entry.path.iter().zip(path.iter()).all(|(a, b)| a == b)
650 })
651}
652
653static ELIMINATION_GENERAL_KEYS: &[&str] = &[
657 "disabledMaps",
658 "enableEnemyHealthBars",
659 "enableKillCam",
660 "enableKillFeed",
661 "enableSkins",
662 "enabledMaps",
663 "gamemodeStartTrigger",
664 "healthPackRespawnTime%",
665 "perkEliminationCatchupLevelAmount%",
666 "perkGeneration%",
667 "spawnHealthPacks",
668 "teamOverlay",
669];
670
671pub(crate) fn mode_inherited_entry(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
672 let [
673 PathPart::Part("gamemodes"),
674 PathPart::Part(mode),
675 PathPart::Part(key),
676 ] = path
677 else {
678 return None;
679 };
680 if *mode == "general" {
681 return None;
682 }
683 let inherits = if *mode == "elimination" {
684 ELIMINATION_GENERAL_KEYS.contains(key)
685 } else {
686 entries().any(|entry| {
689 matches!(
690 entry.path,
691 [PathPart::Part("gamemodes"), PathPart::Part(declared), ..] if *declared == *mode
692 )
693 })
694 };
695 if !inherits {
696 return None;
697 }
698 exact_entry(&[
699 PathPart::Part("gamemodes"),
700 PathPart::Part("general"),
701 PathPart::Part(key),
702 ])
703}
704
705pub(crate) fn entries() -> impl Iterator<Item = &'static TableEntry> {
710 deduplicated_entries(ENTRIES.iter().chain(GENERATED_ENTRIES.iter()))
711}
712
713pub(crate) fn raw_entries() -> impl Iterator<Item = ProjectedEntry> {
717 ENTRIES
718 .iter()
719 .map(|entry| ProjectedEntry {
720 source: ProjectionSource::FixtureTable,
721 entry,
722 })
723 .chain(GENERATED_ENTRIES.iter().map(|entry| ProjectedEntry {
724 source: ProjectionSource::WorkshopDataExport,
725 entry,
726 }))
727}
728
729#[derive(Debug, Clone, Copy)]
731pub(crate) struct ProjectedEntry {
732 pub(crate) source: ProjectionSource,
733 pub(crate) entry: &'static TableEntry,
734}
735
736#[derive(Debug, Clone, Copy)]
738pub(crate) enum ProjectionSource {
739 FixtureTable,
740 WorkshopDataExport,
741}
742
743impl ProjectionSource {
744 pub(crate) const fn label(self) -> &'static str {
745 match self {
746 Self::FixtureTable => "fixture table",
747 Self::WorkshopDataExport => "Workshop-data export",
748 }
749 }
750}
751
752fn deduplicated_entries(
753 entries: impl Iterator<Item = &'static TableEntry>,
754) -> impl Iterator<Item = &'static TableEntry> {
755 let mut paths = std::collections::HashSet::new();
756 entries.filter(move |entry| paths.insert(path_string(entry.path)))
757}
758
759pub(crate) fn is_generated_entry(entry: &TableEntry) -> bool {
760 GENERATED_ENTRIES
761 .iter()
762 .any(|candidate| std::ptr::eq(candidate, entry))
763}
764
765pub(crate) fn ability_slot_for_path(path: &[PathPart<'_>]) -> Option<&'static str> {
769 match path.last() {
770 Some(PathPart::Part("ability1Cooldown%" | "enableAbility1")) => Some("ability1"),
771 Some(PathPart::Part("ability2Cooldown%" | "enableAbility2")) => Some("ability2"),
772 Some(PathPart::Part("ability3Cooldown%" | "enableAbility3")) => Some("ability3"),
773 Some(PathPart::Part(
774 "secondaryFireCooldown%"
775 | "secondaryFireEnergyChargeRate%"
776 | "secondaryFireMaximumTime%"
777 | "secondaryFireRechargeRate%"
778 | "enableSecondaryFire"
779 | "enableGenericSecondaryFire",
780 )) => Some("secondaryFire"),
781 Some(PathPart::Part("combatUltGen%" | "passiveUltGen%" | "ultGen%" | "enableUlt")) => {
782 Some("ultimate")
783 }
784 Some(PathPart::Part("enablePassive")) => Some("passive"),
785 _ => None,
786 }
787}
788
789pub(crate) fn hero_setting_name(hero: &str, key: &str, locale: &str) -> Option<&'static str> {
791 hero_setting_aliases()
794 .iter()
795 .find(|alias| {
796 alias.hero == hero && alias.key == key && alias.locale.eq_ignore_ascii_case(locale)
797 })
798 .map(|alias| alias.display.as_str())
799 .or_else(|| {
800 GENERATED_HERO_SETTING_NAMES
801 .iter()
802 .find(|entry| entry.hero == hero && entry.key == key)
803 .and_then(|entry| entry.localized(locale))
804 })
805 .or_else(|| {
806 if locale.eq_ignore_ascii_case("en-US") {
807 GENERATED_HERO_SETTING_NAMES
808 .iter()
809 .find(|entry| entry.hero == hero && entry.key == key)
810 .filter(|entry| {
811 entry.locales.iter().any(|(known, value)| {
812 known.eq_ignore_ascii_case(locale)
813 && (value.trim().is_empty() || value.starts_with(' '))
814 })
815 })
816 .map(|entry| entry.key)
817 } else {
818 None
819 }
820 })
821}
822
823pub(crate) fn hero_setting_display_matches(
830 hero: &str,
831 key: &str,
832 locale: &str,
833 display: &str,
834) -> bool {
835 if hero_setting_has_alias(hero, key, locale) {
836 return false;
837 }
838 GENERATED_HERO_SETTING_NAMES
839 .iter()
840 .filter(|entry| entry.hero == hero && entry.key == key)
841 .filter_map(|entry| entry.localized(locale))
842 .any(|name| names_eq(name, display))
843}
844
845pub(crate) fn hero_key_template(key: &str) -> Option<&'static str> {
849 GENERATED_ENTRIES
850 .iter()
851 .find(|entry| {
852 matches!(
853 entry.path,
854 [
855 PathPart::Part("heroes"),
856 PathPart::Team,
857 PathPart::Hero,
858 PathPart::Part(candidate)
859 ] if *candidate == key
860 )
861 })
862 .map(|entry| entry.workshop_name)
863 .filter(|name| name.contains("%1$s"))
864}
865
866#[derive(Deserialize)]
867struct HeroSettingAlias {
868 hero: String,
869 key: String,
870 locale: String,
871 display: String,
872}
873
874fn hero_setting_aliases() -> &'static [HeroSettingAlias] {
875 static ALIASES: OnceLock<Vec<HeroSettingAlias>> = OnceLock::new();
876 ALIASES.get_or_init(|| {
877 serde_json::from_str(include_str!("data/hero_setting_aliases.json"))
878 .expect("hero setting alias data is valid JSON")
879 })
880}
881
882pub(crate) fn hero_setting_has_alias(hero: &str, key: &str, locale: &str) -> bool {
885 hero_setting_aliases().iter().any(|alias| {
886 alias.hero == hero && alias.key == key && alias.locale.eq_ignore_ascii_case(locale)
887 })
888}
889
890pub(crate) fn hero_setting_alias(hero: &str, key: &str, locale: &str, display: &str) -> bool {
894 hero_setting_aliases().iter().any(|alias| {
895 alias.hero == hero
896 && alias.key == key
897 && alias.locale.eq_ignore_ascii_case(locale)
898 && names_eq(&alias.display, display)
899 })
900}
901
902fn name_in(maps: &[NameMap], key: &str) -> Option<&'static str> {
903 maps.iter().find(|m| m.key == key).map(|m| m.name)
904}
905
906pub(crate) fn names_eq(left: &str, right: &str) -> bool {
914 left == right
915 || left.to_lowercase() == right.to_lowercase()
916 || crate::catalog::normalize_spelling(left).to_lowercase()
917 == crate::catalog::normalize_spelling(right).to_lowercase()
918}
919
920pub(crate) fn map_spellings() -> impl Iterator<Item = &'static str> {
922 MAP_NAMES
923 .iter()
924 .chain(GENERATED_MAP_NAMES.iter())
925 .map(|m| m.name)
926}
927
928pub(crate) fn hero_spellings() -> impl Iterator<Item = &'static str> {
930 HERO_NAMES
931 .iter()
932 .chain(GENERATED_HERO_NAMES.iter())
933 .map(|m| m.name)
934}
935
936pub(crate) fn team_spellings() -> impl Iterator<Item = &'static str> {
938 TEAM_NAMES.iter().map(|m| m.name)
939}
940
941pub(crate) fn enum_spellings(domain: &str) -> impl Iterator<Item = &'static str> {
943 enum_members(domain).map(|member| member.name)
944}
945
946pub(crate) fn key_spellings(parent: &[PathPart<'_>]) -> Vec<&'static str> {
952 let mut spellings: Vec<&'static str> = entries()
953 .filter(|entry| {
954 entry.path.len() == parent.len() + 1
955 && entry.path[..parent.len()]
956 .iter()
957 .zip(parent.iter())
958 .all(|(a, b)| a == b)
959 && !entry.workshop_name.contains("%1$s")
962 })
963 .map(|entry| entry.workshop_name)
964 .collect();
965 let [PathPart::Part("gamemodes"), PathPart::Part(mode)] = parent else {
966 return spellings;
967 };
968 if *mode == "general" {
969 return spellings;
970 }
971 if *mode == "elimination" {
972 spellings.extend(entries().filter_map(|entry| {
973 let [
974 PathPart::Part("gamemodes"),
975 PathPart::Part("general"),
976 PathPart::Part(key),
977 ] = entry.path
978 else {
979 return None;
980 };
981 ELIMINATION_GENERAL_KEYS
982 .contains(key)
983 .then_some(entry.workshop_name)
984 }));
985 } else if entries().any(|entry| {
986 matches!(
987 entry.path,
988 [PathPart::Part("gamemodes"), PathPart::Part(declared), ..]
989 if *declared == *mode
990 )
991 }) {
992 spellings.extend(entries().filter_map(|entry| {
993 let [
994 PathPart::Part("gamemodes"),
995 PathPart::Part("general"),
996 PathPart::Part(_),
997 ] = entry.path
998 else {
999 return None;
1000 };
1001 (!entry.workshop_name.contains("%1$s")).then_some(entry.workshop_name)
1002 }));
1003 }
1004 spellings
1005}
1006
1007pub(crate) fn mode_name(key: &str) -> Option<&'static str> {
1009 name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
1010}
1011
1012pub(crate) fn map_name(key: &str) -> Option<&'static str> {
1014 name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
1015}
1016
1017pub(crate) fn hero_name(key: &str) -> Option<&'static str> {
1019 name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
1020}
1021
1022pub(crate) fn team_name(key: &str) -> Option<&'static str> {
1024 name_in(TEAM_NAMES, key)
1025}
1026
1027pub(crate) fn enum_name(domain: &str, member: &str) -> Option<&'static str> {
1029 ENUM_MEMBERS
1030 .iter()
1031 .find(|m| m.domain == domain && m.member == member)
1032 .map(|m| m.name)
1033 .or_else(|| {
1034 GENERATED_ENUM_MEMBERS
1035 .iter()
1036 .find(|m| m.domain == domain && m.member == member)
1037 .map(|m| m.name)
1038 })
1039}
1040
1041pub(crate) fn enum_members(domain: &str) -> impl Iterator<Item = &'static EnumMember> {
1042 ENUM_MEMBERS
1043 .iter()
1044 .chain(GENERATED_ENUM_MEMBERS.iter())
1045 .filter(move |member| member.domain == domain)
1046}
1047
1048pub(crate) fn path_string(path: &[PathPart<'_>]) -> String {
1050 path.iter()
1051 .map(|part| match part {
1052 PathPart::Part(name) => (*name).to_string(),
1053 PathPart::Team => "<team>".to_string(),
1054 PathPart::Hero => "<hero>".to_string(),
1055 })
1056 .collect::<Vec<_>>()
1057 .join(".")
1058}