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