1use serde::Deserialize;
12use serde_json::Value;
13use std::sync::OnceLock;
14
15const LOCALE_DATA: &str = include_str!("data/locales.json");
19
20fn locale_data() -> &'static Value {
21 static DATA: OnceLock<Value> = OnceLock::new();
22 DATA.get_or_init(|| {
23 serde_json::from_str(LOCALE_DATA).expect("generated settings locale data is valid JSON")
24 })
25}
26
27pub fn localized_name(locale: &str, section: &str, english: &str) -> Option<&'static str> {
33 let data = locale_data();
34 let aliases = data.get(section)?.get(english)?.as_object()?;
35 aliases.get(locale).and_then(Value::as_str).or_else(|| {
36 aliases.iter().find_map(|(known, value)| {
37 known
38 .eq_ignore_ascii_case(locale)
39 .then(|| value.as_str())
40 .flatten()
41 })
42 })
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum KeyKind {
48 Flag,
50 String,
52 Bool,
54 BoolEnum(&'static str),
57 Number,
59 Percent,
61 Enum(&'static str),
64 ListMap,
66 ListHero,
68}
69
70#[derive(Debug, Clone, Copy, Hash)]
72pub enum PathPart<'a> {
73 Part(&'a str),
76 Team,
78 Hero,
80}
81
82impl<'b> PartialEq<PathPart<'b>> for PathPart<'_> {
83 fn eq(&self, other: &PathPart<'b>) -> bool {
84 match (self, other) {
85 (PathPart::Part(left), PathPart::Part(right)) => left == right,
86 (PathPart::Team, PathPart::Team) => true,
87 (PathPart::Hero, PathPart::Hero) => true,
88 _ => false,
89 }
90 }
91}
92
93impl Eq for PathPart<'_> {}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct TableEntry {
98 pub path: &'static [PathPart<'static>],
99 pub workshop_name: &'static str,
100 pub kind: KeyKind,
101}
102
103macro_rules! entry {
104 ($path:expr, $name:expr, $kind:expr) => {
105 TableEntry {
106 path: &$path,
107 workshop_name: $name,
108 kind: $kind,
109 }
110 };
111}
112
113pub static ENTRIES: &[TableEntry] = &[
125 entry!(
127 [PathPart::Part("main"), PathPart::Part("description")],
128 "Description",
129 KeyKind::String
130 ),
131 entry!(
132 [PathPart::Part("main"), PathPart::Part("modeName")],
133 "Mode Name",
134 KeyKind::String
135 ),
136 entry!(
138 [PathPart::Part("lobby"), PathPart::Part("ffaSlots")],
139 "Max FFA Players",
140 KeyKind::Number
141 ),
142 entry!(
143 [PathPart::Part("lobby"), PathPart::Part("mapRotation")],
144 "Map Rotation",
145 KeyKind::Enum("mapRotation")
146 ),
147 entry!(
148 [PathPart::Part("lobby"), PathPart::Part("spectatorSlots")],
149 "Max Spectators",
150 KeyKind::Number
151 ),
152 entry!(
153 [
154 PathPart::Part("lobby"),
155 PathPart::Part("enableMatchVoiceChat")
156 ],
157 "Match Voice Chat",
158 KeyKind::BoolEnum("matchVoiceChat")
159 ),
160 entry!(
161 [PathPart::Part("lobby"), PathPart::Part("team1Slots")],
162 "Max Team 1 Players",
163 KeyKind::Number
164 ),
165 entry!(
166 [PathPart::Part("lobby"), PathPart::Part("team2Slots")],
167 "Max Team 2 Players",
168 KeyKind::Number
169 ),
170 entry!(
171 [PathPart::Part("lobby"), PathPart::Part("returnToLobby")],
172 "Return To Lobby",
173 KeyKind::Enum("returnToLobby")
174 ),
175 entry!(
176 [
177 PathPart::Part("lobby"),
178 PathPart::Part("allowPlayersInQueue")
179 ],
180 "Allow Players Who Are In Queue",
181 KeyKind::Bool
182 ),
183 entry!(
184 [
185 PathPart::Part("lobby"),
186 PathPart::Part("swapTeamsAfterMatch")
187 ],
188 "Swap Teams After Match",
189 KeyKind::Bool
190 ),
191 entry!(
198 [
199 PathPart::Part("gamemodes"),
200 PathPart::Part("assault"),
201 PathPart::Part("enabled")
202 ],
203 "enabled",
204 KeyKind::Bool
205 ),
206 entry!(
207 [
208 PathPart::Part("gamemodes"),
209 PathPart::Part("control"),
210 PathPart::Part("enabled")
211 ],
212 "enabled",
213 KeyKind::Bool
214 ),
215 entry!(
216 [
217 PathPart::Part("gamemodes"),
218 PathPart::Part("escort"),
219 PathPart::Part("enabled")
220 ],
221 "enabled",
222 KeyKind::Bool
223 ),
224 entry!(
225 [
226 PathPart::Part("gamemodes"),
227 PathPart::Part("hybrid"),
228 PathPart::Part("enabled")
229 ],
230 "enabled",
231 KeyKind::Bool
232 ),
233 entry!(
234 [
235 PathPart::Part("gamemodes"),
236 PathPart::Part("assault"),
237 PathPart::Part("enabledMaps")
238 ],
239 "enabled maps",
240 KeyKind::ListMap
241 ),
242 entry!(
243 [
244 PathPart::Part("gamemodes"),
245 PathPart::Part("control"),
246 PathPart::Part("enabledMaps")
247 ],
248 "enabled maps",
249 KeyKind::ListMap
250 ),
251 entry!(
252 [
253 PathPart::Part("gamemodes"),
254 PathPart::Part("escort"),
255 PathPart::Part("enabledMaps")
256 ],
257 "enabled maps",
258 KeyKind::ListMap
259 ),
260 entry!(
261 [
262 PathPart::Part("gamemodes"),
263 PathPart::Part("hybrid"),
264 PathPart::Part("enabledMaps")
265 ],
266 "enabled maps",
267 KeyKind::ListMap
268 ),
269 entry!(
270 [
271 PathPart::Part("gamemodes"),
272 PathPart::Part("skirmish"),
273 PathPart::Part("enabledMaps")
274 ],
275 "enabled maps",
276 KeyKind::ListMap
277 ),
278 entry!(
279 [
280 PathPart::Part("gamemodes"),
281 PathPart::Part("assault"),
282 PathPart::Part("disabledMaps")
283 ],
284 "disabled maps",
285 KeyKind::ListMap
286 ),
287 entry!(
288 [
289 PathPart::Part("gamemodes"),
290 PathPart::Part("skirmish"),
291 PathPart::Part("disabledMaps")
292 ],
293 "disabled maps",
294 KeyKind::ListMap
295 ),
296 entry!(
297 [
298 PathPart::Part("gamemodes"),
299 PathPart::Part("ffa"),
300 PathPart::Part("enabledMaps")
301 ],
302 "enabled maps",
303 KeyKind::ListMap
304 ),
305 entry!(
306 [
307 PathPart::Part("gamemodes"),
308 PathPart::Part("tdm"),
309 PathPart::Part("enabledMaps")
310 ],
311 "enabled maps",
312 KeyKind::ListMap
313 ),
314 entry!(
315 [
316 PathPart::Part("gamemodes"),
317 PathPart::Part("assault"),
318 PathPart::Part("roleLimit")
319 ],
320 "Limit Roles",
321 KeyKind::Enum("roleLimit")
322 ),
323 entry!(
324 [
325 PathPart::Part("gamemodes"),
326 PathPart::Part("control"),
327 PathPart::Part("roleLimit")
328 ],
329 "Limit Roles",
330 KeyKind::Enum("roleLimit")
331 ),
332 entry!(
333 [
334 PathPart::Part("gamemodes"),
335 PathPart::Part("escort"),
336 PathPart::Part("roleLimit")
337 ],
338 "Limit Roles",
339 KeyKind::Enum("roleLimit")
340 ),
341 entry!(
342 [
343 PathPart::Part("gamemodes"),
344 PathPart::Part("hybrid"),
345 PathPart::Part("roleLimit")
346 ],
347 "Limit Roles",
348 KeyKind::Enum("roleLimit")
349 ),
350 entry!(
351 [
352 PathPart::Part("gamemodes"),
353 PathPart::Part("general"),
354 PathPart::Part("roleLimit")
355 ],
356 "Limit Roles",
357 KeyKind::Enum("roleLimit")
358 ),
359 entry!(
360 [
361 PathPart::Part("gamemodes"),
362 PathPart::Part("assault"),
363 PathPart::Part("enableCompetitiveRules")
364 ],
365 "Competitive Rules",
366 KeyKind::Bool
367 ),
368 entry!(
369 [
370 PathPart::Part("gamemodes"),
371 PathPart::Part("control"),
372 PathPart::Part("enableCompetitiveRules")
373 ],
374 "Competitive Rules",
375 KeyKind::Bool
376 ),
377 entry!(
378 [
379 PathPart::Part("gamemodes"),
380 PathPart::Part("escort"),
381 PathPart::Part("enableCompetitiveRules")
382 ],
383 "Competitive Rules",
384 KeyKind::Bool
385 ),
386 entry!(
387 [
388 PathPart::Part("gamemodes"),
389 PathPart::Part("hybrid"),
390 PathPart::Part("enableCompetitiveRules")
391 ],
392 "Competitive Rules",
393 KeyKind::Bool
394 ),
395 entry!(
397 [
398 PathPart::Part("gamemodes"),
399 PathPart::Part("general"),
400 PathPart::Part("enableCompetitiveRules")
401 ],
402 "Competitive Rules",
403 KeyKind::Bool
404 ),
405 entry!(
406 [
407 PathPart::Part("gamemodes"),
408 PathPart::Part("general"),
409 PathPart::Part("enablePerks")
410 ],
411 "Enable Perks",
412 KeyKind::Bool
413 ),
414 entry!(
415 [
416 PathPart::Part("gamemodes"),
417 PathPart::Part("general"),
418 PathPart::Part("heroLimit")
419 ],
420 "Hero Limit",
421 KeyKind::Enum("heroLimit")
422 ),
423 entry!(
424 [
425 PathPart::Part("gamemodes"),
426 PathPart::Part("general"),
427 PathPart::Part("respawnTime%")
428 ],
429 "Respawn Time Scalar",
430 KeyKind::Percent
431 ),
432 entry!(
433 [
434 PathPart::Part("gamemodes"),
435 PathPart::Part("general"),
436 PathPart::Part("enableHeroSwitching")
437 ],
438 "Allow Hero Switching",
439 KeyKind::Bool
440 ),
441 entry!(
442 [
443 PathPart::Part("gamemodes"),
444 PathPart::Part("general"),
445 PathPart::Part("enableRandomHeroes")
446 ],
447 "Respawn As Random Hero",
448 KeyKind::Bool
449 ),
450 entry!(
451 [
452 PathPart::Part("gamemodes"),
453 PathPart::Part("general"),
454 PathPart::Part("gameModeStartTrigger")
455 ],
456 "Game Mode Start",
457 KeyKind::Enum("gameModeStartTrigger")
458 ),
459 entry!(
460 [
461 PathPart::Part("gamemodes"),
462 PathPart::Part("assault"),
463 PathPart::Part("gameModeStartTrigger")
464 ],
465 "Game Mode Start",
466 KeyKind::Enum("gameModeStartTrigger")
467 ),
468 entry!(
469 [
470 PathPart::Part("gamemodes"),
471 PathPart::Part("assault"),
472 PathPart::Part("tankPassiveHealthBonus")
473 ],
474 "Tank Role Passive Health Bonus",
475 KeyKind::Enum("tankPassiveHealthBonus")
476 ),
477 entry!(
478 [
479 PathPart::Part("gamemodes"),
480 PathPart::Part("general"),
481 PathPart::Part("spawnHealthPacks")
482 ],
483 "Spawn Health Packs",
484 KeyKind::Enum("spawnHealthPacks")
485 ),
486 entry!(
488 [
489 PathPart::Part("heroes"),
490 PathPart::Team,
491 PathPart::Part("enabledHeroes")
492 ],
493 "enabled heroes",
494 KeyKind::ListHero
495 ),
496 entry!(
497 [
498 PathPart::Part("heroes"),
499 PathPart::Team,
500 PathPart::Part("disabledHeroes")
501 ],
502 "disabled heroes",
503 KeyKind::ListHero
504 ),
505 entry!(
506 [
507 PathPart::Part("heroes"),
508 PathPart::Part("general"),
509 PathPart::Part("disabledHeroes")
510 ],
511 "disabled heroes",
512 KeyKind::ListHero
513 ),
514 entry!(
516 [
517 PathPart::Part("heroes"),
518 PathPart::Team,
519 PathPart::Hero,
520 PathPart::Part("enablePrimaryFire")
521 ],
522 "Primary Fire",
523 KeyKind::Bool
524 ),
525 entry!(
526 [
527 PathPart::Part("heroes"),
528 PathPart::Team,
529 PathPart::Hero,
530 PathPart::Part("enableSecondaryFire")
531 ],
532 "Secondary Fire",
533 KeyKind::Bool
534 ),
535 entry!(
536 [
537 PathPart::Part("heroes"),
538 PathPart::Team,
539 PathPart::Hero,
540 PathPart::Part("enableAbility1")
541 ],
542 "Ability 1",
543 KeyKind::Bool
544 ),
545 entry!(
546 [
547 PathPart::Part("heroes"),
548 PathPart::Team,
549 PathPart::Hero,
550 PathPart::Part("enableAbility2")
551 ],
552 "Ability 2",
553 KeyKind::Bool
554 ),
555 entry!(
556 [
557 PathPart::Part("heroes"),
558 PathPart::Team,
559 PathPart::Hero,
560 PathPart::Part("health%")
561 ],
562 "Health",
563 KeyKind::Percent
564 ),
565 entry!(
566 [
567 PathPart::Part("heroes"),
568 PathPart::Team,
569 PathPart::Hero,
570 PathPart::Part("passiveUltGen%")
571 ],
572 "Ultimate Generation - Passive Blizzard",
573 KeyKind::Percent
574 ),
575 entry!(
576 [
577 PathPart::Part("heroes"),
578 PathPart::Team,
579 PathPart::Hero,
580 PathPart::Part("combatUltGen%")
581 ],
582 "Ultimate Generation - Combat Blizzard",
583 KeyKind::Percent
584 ),
585];
586
587#[derive(Debug, Clone, Copy)]
589pub struct NameMap {
590 pub key: &'static str,
591 pub name: &'static str,
592}
593
594pub static MODE_NAMES: &[NameMap] = &[
597 NameMap {
598 key: "assault",
599 name: "Assault",
600 },
601 NameMap {
602 key: "control",
603 name: "Control",
604 },
605 NameMap {
606 key: "escort",
607 name: "Escort",
608 },
609 NameMap {
610 key: "hybrid",
611 name: "Hybrid",
612 },
613 NameMap {
614 key: "skirmish",
615 name: "Skirmish",
616 },
617 NameMap {
618 key: "ffa",
619 name: "Deathmatch",
620 },
621 NameMap {
622 key: "tdm",
623 name: "Team Deathmatch",
624 },
625 NameMap {
626 key: "general",
627 name: "General",
628 },
629];
630
631pub static MAP_NAMES: &[NameMap] = &[
633 NameMap {
634 key: "workshopIsland",
635 name: "Workshop Island",
636 },
637 NameMap {
638 key: "kingsRowWinter",
639 name: "King's Row Winter",
640 },
641];
642
643pub static HERO_NAMES: &[NameMap] = &[
645 NameMap {
646 key: "anran",
647 name: "Anran",
648 },
649 NameMap {
650 key: "ana",
651 name: "Ana",
652 },
653 NameMap {
654 key: "ashe",
655 name: "Ashe",
656 },
657 NameMap {
658 key: "bastion",
659 name: "Bastion",
660 },
661 NameMap {
662 key: "baptiste",
663 name: "Baptiste",
664 },
665 NameMap {
666 key: "brigitte",
667 name: "Brigitte",
668 },
669 NameMap {
670 key: "cassidy",
671 name: "Cassidy",
672 },
673 NameMap {
674 key: "dmon",
675 name: "D.Mon",
676 },
677 NameMap {
678 key: "domina",
679 name: "Domina",
680 },
681 NameMap {
682 key: "dva",
683 name: "D.Va",
684 },
685 NameMap {
686 key: "doomfist",
687 name: "Doomfist",
688 },
689 NameMap {
690 key: "echo",
691 name: "Echo",
692 },
693 NameMap {
694 key: "emre",
695 name: "Emre",
696 },
697 NameMap {
698 key: "freja",
699 name: "Freja",
700 },
701 NameMap {
702 key: "genji",
703 name: "Genji",
704 },
705 NameMap {
706 key: "hanzo",
707 name: "Hanzo",
708 },
709 NameMap {
710 key: "moira",
711 name: "Moira",
712 },
713 NameMap {
714 key: "reinhardt",
715 name: "Reinhardt",
716 },
717 NameMap {
718 key: "hammond",
719 name: "Wrecking Ball",
720 },
721 NameMap {
722 key: "hazard",
723 name: "Hazard",
724 },
725 NameMap {
726 key: "illari",
727 name: "Illari",
728 },
729 NameMap {
730 key: "juno",
731 name: "Juno",
732 },
733 NameMap {
734 key: "jetpackCat",
735 name: "Jetpack Cat",
736 },
737 NameMap {
738 key: "junkerQueen",
739 name: "Junker Queen",
740 },
741 NameMap {
742 key: "junkrat",
743 name: "Junkrat",
744 },
745 NameMap {
746 key: "kiriko",
747 name: "Kiriko",
748 },
749 NameMap {
750 key: "lucio",
751 name: "Lúcio",
752 },
753 NameMap {
754 key: "mauga",
755 name: "Mauga",
756 },
757 NameMap {
758 key: "mercy",
759 name: "Mercy",
760 },
761 NameMap {
762 key: "mizuki",
763 name: "Mizuki",
764 },
765 NameMap {
766 key: "orisa",
767 name: "Orisa",
768 },
769 NameMap {
770 key: "pharah",
771 name: "Pharah",
772 },
773 NameMap {
774 key: "reaper",
775 name: "Reaper",
776 },
777 NameMap {
778 key: "roadhog",
779 name: "Roadhog",
780 },
781 NameMap {
782 key: "shion",
783 name: "Shion",
784 },
785 NameMap {
786 key: "sierra",
787 name: "Sierra",
788 },
789 NameMap {
790 key: "sigma",
791 name: "Sigma",
792 },
793 NameMap {
794 key: "ramattra",
795 name: "Ramattra",
796 },
797 NameMap {
798 key: "lifeweaver",
799 name: "Lifeweaver",
800 },
801 NameMap {
802 key: "sojourn",
803 name: "Sojourn",
804 },
805 NameMap {
806 key: "soldier",
807 name: "Soldier: 76",
808 },
809 NameMap {
810 key: "sombra",
811 name: "Sombra",
812 },
813 NameMap {
814 key: "symmetra",
815 name: "Symmetra",
816 },
817 NameMap {
818 key: "torbjorn",
819 name: "Torbjörn",
820 },
821 NameMap {
822 key: "tracer",
823 name: "Tracer",
824 },
825 NameMap {
826 key: "venture",
827 name: "Venture",
828 },
829 NameMap {
830 key: "widowmaker",
831 name: "Widowmaker",
832 },
833 NameMap {
834 key: "winston",
835 name: "Winston",
836 },
837 NameMap {
838 key: "wuyang",
839 name: "Wuyang",
840 },
841 NameMap {
842 key: "wreckingBall",
843 name: "Wrecking Ball",
844 },
845 NameMap {
846 key: "zarya",
847 name: "Zarya",
848 },
849 NameMap {
850 key: "zenyatta",
851 name: "Zenyatta",
852 },
853 NameMap {
854 key: "mei",
855 name: "Mei",
856 },
857];
858
859include!("data/generated_map_entries.rs");
860include!("data/generated_hero_entries.rs");
861include!("data/generated_mode_entries.rs");
862
863pub static TEAM_NAMES: &[NameMap] = &[
865 NameMap {
866 key: "allTeams",
867 name: "General",
868 },
869 NameMap {
870 key: "team1",
871 name: "Team 1",
872 },
873 NameMap {
874 key: "team2",
875 name: "Team 2",
876 },
877];
878
879#[derive(Debug, Clone, Copy)]
881pub struct EnumMember {
882 pub domain: &'static str,
883 pub member: &'static str,
884 pub name: &'static str,
885}
886
887include!("data/generated_entries.rs");
888include!("data/generated_hero_settings.rs");
889
890pub static ENUM_MEMBERS: &[EnumMember] = &[
895 EnumMember {
896 domain: "mapRotation",
897 member: "afterAGame",
898 name: "After A Game",
899 },
900 EnumMember {
901 domain: "mapRotation",
902 member: "afterMirrorMatch",
903 name: "After A Mirror Match",
904 },
905 EnumMember {
906 domain: "mapRotation",
907 member: "paused",
908 name: "Paused",
909 },
910 EnumMember {
911 domain: "matchVoiceChat",
912 member: "enabled",
913 name: "Enabled",
914 },
915 EnumMember {
916 domain: "returnToLobby",
917 member: "never",
918 name: "Never",
919 },
920 EnumMember {
921 domain: "returnToLobby",
922 member: "afterAGame",
923 name: "After A Game",
924 },
925 EnumMember {
926 domain: "returnToLobby",
927 member: "afterMirrorMatch",
928 name: "After A Mirror Match",
929 },
930 EnumMember {
931 domain: "gameModeStartTrigger",
932 member: "immediately",
933 name: "Immediately",
934 },
935 EnumMember {
936 domain: "gameModeStartTrigger",
937 member: "manual",
938 name: "Manual",
939 },
940 EnumMember {
941 domain: "spawnHealthPacks",
942 member: "disabled",
943 name: "Disabled",
944 },
945 EnumMember {
946 domain: "spawnHealthPacks",
947 member: "modeDependent",
948 name: "Determined By Mode",
949 },
950 EnumMember {
951 domain: "spawnHealthPacks",
952 member: "enabled",
953 name: "Enabled",
954 },
955 EnumMember {
956 domain: "roleLimit",
957 member: "2OfEachRolePerTeam",
958 name: "2 Of Each Role Per Team",
959 },
960 EnumMember {
961 domain: "roleLimit",
962 member: "1Tank2Offense2Support",
963 name: "1 Tank 2 Offense 2 Support",
964 },
965 EnumMember {
966 domain: "roleLimit",
967 member: "off",
968 name: "Off",
969 },
970 EnumMember {
971 domain: "tankPassiveHealthBonus",
972 member: "alwaysEnabled",
973 name: "Always Enabled",
974 },
975 EnumMember {
976 domain: "tankPassiveHealthBonus",
977 member: "disabled",
978 name: "Disabled",
979 },
980 EnumMember {
981 domain: "heroLimit",
982 member: "off",
983 name: "Off",
984 },
985 EnumMember {
986 domain: "heroLimit",
987 member: "1PerTeam",
988 name: "1 Per Team",
989 },
990 EnumMember {
991 domain: "heroLimit",
992 member: "2PerTeam",
993 name: "2 Per Team",
994 },
995 EnumMember {
996 domain: "heroLimit",
997 member: "1PerGame",
998 name: "1 Per Game",
999 },
1000 EnumMember {
1001 domain: "heroLimit",
1002 member: "2PerGame",
1003 name: "2 Per Game",
1004 },
1005];
1006
1007pub fn lookup(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
1009 entries().find(|entry| {
1010 entry.path.len() == path.len() && entry.path.iter().zip(path.iter()).all(|(a, b)| a == b)
1011 })
1012}
1013
1014pub fn entries() -> impl Iterator<Item = &'static TableEntry> {
1019 deduplicated_entries(ENTRIES.iter().chain(GENERATED_ENTRIES.iter()))
1020}
1021
1022pub(crate) fn raw_entries() -> impl Iterator<Item = ProjectedEntry> {
1026 ENTRIES
1027 .iter()
1028 .map(|entry| ProjectedEntry {
1029 source: ProjectionSource::FixtureTable,
1030 entry,
1031 })
1032 .chain(GENERATED_ENTRIES.iter().map(|entry| ProjectedEntry {
1033 source: ProjectionSource::WorkshopDataExport,
1034 entry,
1035 }))
1036}
1037
1038#[derive(Debug, Clone, Copy)]
1040pub(crate) struct ProjectedEntry {
1041 pub(crate) source: ProjectionSource,
1042 pub(crate) entry: &'static TableEntry,
1043}
1044
1045#[derive(Debug, Clone, Copy)]
1047pub(crate) enum ProjectionSource {
1048 FixtureTable,
1049 WorkshopDataExport,
1050}
1051
1052impl ProjectionSource {
1053 pub(crate) const fn label(self) -> &'static str {
1054 match self {
1055 Self::FixtureTable => "fixture table",
1056 Self::WorkshopDataExport => "Workshop-data export",
1057 }
1058 }
1059}
1060
1061fn deduplicated_entries(
1062 entries: impl Iterator<Item = &'static TableEntry>,
1063) -> impl Iterator<Item = &'static TableEntry> {
1064 let mut paths = std::collections::HashSet::new();
1065 entries.filter(move |entry| paths.insert(path_string(entry.path)))
1066}
1067
1068pub(crate) fn is_generated_entry(entry: &TableEntry) -> bool {
1069 GENERATED_ENTRIES
1070 .iter()
1071 .any(|candidate| std::ptr::eq(candidate, entry))
1072}
1073
1074pub fn ability_slot_for_path(path: &[PathPart<'_>]) -> Option<&'static str> {
1078 match path.last() {
1079 Some(PathPart::Part("ability1Cooldown%" | "enableAbility1")) => Some("ability1"),
1080 Some(PathPart::Part("ability2Cooldown%" | "enableAbility2")) => Some("ability2"),
1081 Some(PathPart::Part("ability3Cooldown%" | "enableAbility3")) => Some("ability3"),
1082 Some(PathPart::Part(
1083 "secondaryFireCooldown%"
1084 | "secondaryFireEnergyChargeRate%"
1085 | "secondaryFireMaximumTime%"
1086 | "secondaryFireRechargeRate%"
1087 | "enableSecondaryFire"
1088 | "enableGenericSecondaryFire",
1089 )) => Some("secondaryFire"),
1090 Some(PathPart::Part("combatUltGen%" | "passiveUltGen%" | "ultGen%" | "enableUlt")) => {
1091 Some("ultimate")
1092 }
1093 Some(PathPart::Part("enablePassive")) => Some("passive"),
1094 Some(PathPart::Part("enableAutomaticFire" | "enableScoping")) => Some("primaryFire"),
1095 _ => None,
1096 }
1097}
1098
1099pub fn hero_setting_name(hero: &str, key: &str, locale: &str) -> Option<&'static str> {
1101 let generated = GENERATED_HERO_SETTING_NAMES
1102 .iter()
1103 .find(|entry| entry.hero == hero && entry.key == key)
1104 .and_then(|entry| entry.localized(locale));
1105 generated
1106 .or_else(|| {
1107 hero_setting_aliases()
1108 .iter()
1109 .find(|alias| {
1110 alias.hero == hero
1111 && alias.key == key
1112 && alias.locale.eq_ignore_ascii_case(locale)
1113 })
1114 .map(|alias| alias.display.as_str())
1115 })
1116 .or_else(|| {
1117 if locale.eq_ignore_ascii_case("en-US") {
1118 GENERATED_HERO_SETTING_NAMES
1119 .iter()
1120 .find(|entry| entry.hero == hero && entry.key == key)
1121 .filter(|entry| {
1122 entry.locales.iter().any(|(known, value)| {
1123 known.eq_ignore_ascii_case(locale)
1124 && (value.trim().is_empty() || value.starts_with(' '))
1125 })
1126 })
1127 .map(|entry| entry.key)
1128 } else {
1129 None
1130 }
1131 })
1132}
1133
1134#[derive(Deserialize)]
1135struct HeroSettingAlias {
1136 hero: String,
1137 key: String,
1138 locale: String,
1139 display: String,
1140}
1141
1142fn hero_setting_aliases() -> &'static [HeroSettingAlias] {
1143 static ALIASES: OnceLock<Vec<HeroSettingAlias>> = OnceLock::new();
1144 ALIASES.get_or_init(|| {
1145 serde_json::from_str(include_str!("data/hero_setting_aliases.json"))
1146 .expect("hero setting alias data is valid JSON")
1147 })
1148}
1149
1150pub fn hero_setting_alias(hero: &str, key: &str, locale: &str, display: &str) -> bool {
1154 hero_setting_aliases().iter().any(|alias| {
1155 alias.hero == hero
1156 && alias.key == key
1157 && alias.locale.eq_ignore_ascii_case(locale)
1158 && alias.display == display
1159 })
1160}
1161
1162fn name_in(maps: &[NameMap], key: &str) -> Option<&'static str> {
1163 maps.iter().find(|m| m.key == key).map(|m| m.name)
1164}
1165
1166pub fn mode_name(key: &str) -> Option<&'static str> {
1168 name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
1169}
1170
1171pub fn map_name(key: &str) -> Option<&'static str> {
1173 name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
1174}
1175
1176pub fn hero_name(key: &str) -> Option<&'static str> {
1178 name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
1179}
1180
1181pub fn team_name(key: &str) -> Option<&'static str> {
1183 name_in(TEAM_NAMES, key)
1184}
1185
1186pub fn enum_name(domain: &str, member: &str) -> Option<&'static str> {
1188 ENUM_MEMBERS
1189 .iter()
1190 .find(|m| m.domain == domain && m.member == member)
1191 .map(|m| m.name)
1192 .or_else(|| {
1193 GENERATED_ENUM_MEMBERS
1194 .iter()
1195 .find(|m| m.domain == domain && m.member == member)
1196 .map(|m| m.name)
1197 })
1198}
1199
1200pub fn path_string(path: &[PathPart<'_>]) -> String {
1202 path.iter()
1203 .map(|part| match part {
1204 PathPart::Part(name) => (*name).to_string(),
1205 PathPart::Team => "<team>".to_string(),
1206 PathPart::Hero => "<hero>".to_string(),
1207 })
1208 .collect::<Vec<_>>()
1209 .join(".")
1210}