Skip to main content

workshop_rs/settings/
table.rs

1//! Fixture-evidenced settings emission table (#86).
2//!
3//! PROVENANCE: observed from the pinned oracle 9.7.10 en-US output of the
4//! oracle-success settings programs (`compile.workshop` settings section of
5//! the committed snapshots pixelart/santa/broken-weapons/client-to-server,
6//! plus the parabola/crosshair/inputhud oracle runs) at OverPy commit
7//! `eea67ad`. This is observed-behavior data, not copied OverPy source
8//! (LICENSE-BOUNDARY policy). Additions to the table (e.g. the acquired
9//! candidate snapshots) are data-only.
10
11use serde::Deserialize;
12use serde_json::Value;
13use std::sync::OnceLock;
14
15/// Locale-specific settings names generated from the reviewed Workshop data
16/// export. The projection contains every reviewed locale as data; adding a
17/// locale changes this file, not the parser or emitter architecture.
18const 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
27/// Resolve a settings display name from the generated locale corpus.
28///
29/// The English table names are intentionally not duplicated in the locale
30/// data. A missing entry means the target locale is not covered and callers
31/// must preserve the explicit missing-mapping contract.
32pub 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/// A leaf key kind: how a settings leaf renders and validates.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum KeyKind {
48    /// A presence-only extension setting.
49    Flag,
50    /// A quoted string (`Description: "..."`).
51    String,
52    /// A boolean rendered `On`/`Off`.
53    Bool,
54    /// A boolean carrier rendered through an evidenced true-value enum token.
55    /// The false value remains unsupported until independently evidenced.
56    BoolEnum(&'static str),
57    /// A plain number.
58    Number,
59    /// A number rendered with a `%` suffix (`Respawn Time Scalar: 30%`).
60    Percent,
61    /// A string-valued enumeration with a per-domain member map
62    /// (`Enum(domain)`).
63    Enum(&'static str),
64    /// A list of map names (`enabled maps`).
65    ListMap,
66    /// A list of hero names (`enabled heroes`).
67    ListHero,
68}
69
70/// One segment of an exact settings path.
71#[derive(Debug, Clone, Copy, Hash)]
72pub enum PathPart<'a> {
73    /// A literal key (mode names under `gamemodes` are literal keys too:
74    /// per-key subsets are exact-path entries, #86).
75    Part(&'a str),
76    /// Any team slot (allTeams), rendered through [`team_name`].
77    Team,
78    /// Any hero-config slot, rendered through [`hero_name`].
79    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/// One table entry: an exact key path, its workshop name, and its kind.
96#[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
113/// The fixture-evidenced settings surface.
114///
115/// Slot sets (evidenced): teams {allTeams}, heroes {mei} config groups +
116/// the 10 ListHero names. `enabled: true` is not evidenced; it renders with
117/// no prefix. Keys outside this table (e.g. team1Slots, scoreToWin,
118/// gamemodeStartTrigger, spawnHealthPacks, healthPackRespawnTime%,
119/// abilityCooldown%, healingReceived%, primaryFireKb%, enableSpawningWithUlt,
120/// resetPlayersAfterGoalScored, scoreLeadToWin, gameLengthInSec,
121/// heroes.<team>.general, roleLimit under general, heroLimit under a named
122/// mode) are `settings-unknown-key` at validation (only evidenced in
123/// oracle-failing programs; corpus-bounded).
124pub static ENTRIES: &[TableEntry] = &[
125    // main
126    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    // lobby
137    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    // gamemodes.<mode> — per-key subsets (exact-path entries, #86):
192    // enabledMaps under modes {assault, control, escort, hybrid, skirmish,
193    // ffa}; enabled/roleLimit/enableCompetitiveRules under {assault, control,
194    // escort, hybrid}; heroLimit/respawnTime%/enableHeroSwitching/
195    // enableRandomHeroes under general only (general is a literal group name,
196    // not a mode slot).
197    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    // gamemodes.general
396    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    // heroes.<team>
487    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    // heroes.<team>.<hero> config groups
515    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/// A slot name mapping (key -> localized workshop name).
588#[derive(Debug, Clone, Copy)]
589pub struct NameMap {
590    pub key: &'static str,
591    pub name: &'static str,
592}
593
594/// Game-mode names (evidenced: assault, control, escort, hybrid, skirmish,
595/// ffa, tdm, general).
596pub 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
631/// Map names inside `enabledMaps` lists.
632pub 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
643/// Hero names inside hero lists and hero-config groups.
644pub 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
863/// Team names inside `heroes` (evidenced: allTeams).
864pub 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/// An enum domain member (domain -> localized workshop name).
880#[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
890/// Fixture-owned canonical enum member names. Additional reviewed
891/// Workshop-data export members are retained through
892/// `projection_reconciliation.json`, which maps their source identities into
893/// these canonical domains without replacing fixture-backed display names.
894pub 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
1007/// Look up a settings leaf entry by its exact path.
1008pub 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
1014/// Iterate the reviewed settings inventory with the hand-written projection
1015/// taking precedence over the generated export projection. Duplicate paths
1016/// are represented once in the semantic catalog while the parser and emitter
1017/// continue to use the same lookup table.
1018pub fn entries() -> impl Iterator<Item = &'static TableEntry> {
1019    deduplicated_entries(ENTRIES.iter().chain(GENERATED_ENTRIES.iter()))
1020}
1021
1022/// Iterate both catalog projections without applying effective lookup
1023/// precedence. The semantic validator uses this to compare duplicate paths
1024/// instead of allowing `entries()` to hide stale or conflicting data.
1025pub(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/// One setting leaf before effective lookup resolves duplicated projections.
1039#[derive(Debug, Clone, Copy)]
1040pub(crate) struct ProjectedEntry {
1041    pub(crate) source: ProjectionSource,
1042    pub(crate) entry: &'static TableEntry,
1043}
1044
1045/// The source that supplied a raw settings projection entry.
1046#[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
1074/// Map the existing hero-settings leaf keys to canonical gameplay slots.
1075/// The setting tree remains the owner of the keys; display names are resolved
1076/// from the gameplay catalog by the parser/emitter when a hero context exists.
1077pub 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
1099/// Resolve an evidence-backed hero-specific setting label.
1100pub 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
1150/// Reviewed producer aliases observed in the pinned AI-PVE artifact. These
1151/// labels omit the export's `倍率` suffix or use the producer's shorter
1152/// ability label, but identify the same canonical setting path.
1153pub 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
1166/// The localized name of a game mode.
1167pub fn mode_name(key: &str) -> Option<&'static str> {
1168    name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
1169}
1170
1171/// The localized name of a map.
1172pub fn map_name(key: &str) -> Option<&'static str> {
1173    name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
1174}
1175
1176/// The localized name of a hero.
1177pub fn hero_name(key: &str) -> Option<&'static str> {
1178    name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
1179}
1180
1181/// The localized name of a team.
1182pub fn team_name(key: &str) -> Option<&'static str> {
1183    name_in(TEAM_NAMES, key)
1184}
1185
1186/// The localized name of an enum member in a domain.
1187pub 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
1200/// A human-readable rendering of a path (diagnostics).
1201pub 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}