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