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 plain number.
55    Number,
56    /// A number rendered with a `%` suffix (`Respawn Time Scalar: 30%`).
57    Percent,
58    /// A string-valued enumeration with a per-domain member map
59    /// (`Enum(domain)`).
60    Enum(&'static str),
61    /// A list of map names (`enabled maps`).
62    ListMap,
63    /// A list of hero names (`enabled heroes`).
64    ListHero,
65}
66
67/// One segment of an exact settings path.
68#[derive(Debug, Clone, Copy, Hash)]
69pub enum PathPart<'a> {
70    /// A literal key (mode names under `gamemodes` are literal keys too:
71    /// per-key subsets are exact-path entries, #86).
72    Part(&'a str),
73    /// Any team slot (allTeams), rendered through [`team_name`].
74    Team,
75    /// Any hero-config slot, rendered through [`hero_name`].
76    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/// One table entry: an exact key path, its workshop name, and its kind.
93#[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
110/// The fixture-evidenced settings surface.
111///
112/// Slot sets (evidenced): teams {allTeams}, heroes {mei} config groups +
113/// the 10 ListHero names. `enabled: true` is not evidenced; it renders with
114/// no prefix. Keys outside this table (e.g. team1Slots, scoreToWin,
115/// gamemodeStartTrigger, spawnHealthPacks, healthPackRespawnTime%,
116/// abilityCooldown%, healingReceived%, primaryFireKb%, enableSpawningWithUlt,
117/// resetPlayersAfterGoalScored, scoreLeadToWin, gameLengthInSec,
118/// heroes.<team>.general, roleLimit under general, heroLimit under a named
119/// mode) are `settings-unknown-key` at validation (only evidenced in
120/// oracle-failing programs; corpus-bounded).
121pub static ENTRIES: &[TableEntry] = &[
122    // main
123    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    // lobby
134    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    // gamemodes.<mode> — per-key subsets (exact-path entries, #86):
186    // enabledMaps under modes {assault, control, escort, hybrid, skirmish,
187    // ffa}; enabled/roleLimit/enableCompetitiveRules under {assault, control,
188    // escort, hybrid}; heroLimit/respawnTime%/enableHeroSwitching/
189    // enableRandomHeroes under general only (general is a literal group name,
190    // not a mode slot).
191    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    // gamemodes.general
390    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    // heroes.<team>
481    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    // heroes.<team>.<hero> config groups
509    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/// A slot name mapping (key -> localized workshop name).
582#[derive(Debug, Clone, Copy)]
583pub struct NameMap {
584    pub key: &'static str,
585    pub name: &'static str,
586}
587
588/// Game-mode names (evidenced: assault, control, escort, hybrid, skirmish,
589/// ffa, tdm, general).
590pub 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
625/// Map names inside `enabledMaps` lists.
626pub 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
637/// Hero names inside hero lists and hero-config groups.
638pub 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
857/// Team names inside `heroes` (evidenced: allTeams).
858pub 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/// An enum domain member (domain -> localized workshop name).
874#[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
884/// Fixture-owned canonical enum member names. Additional reviewed
885/// Workshop-data export members are retained through
886/// `projection_reconciliation.json`, which maps their source identities into
887/// these canonical domains without replacing fixture-backed display names.
888pub 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
1001/// Look up a settings leaf entry by its exact path.
1002pub 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
1008/// Iterate the reviewed settings inventory with the hand-written projection
1009/// taking precedence over the generated export projection. Duplicate paths
1010/// are represented once in the semantic catalog while the parser and emitter
1011/// continue to use the same lookup table.
1012pub fn entries() -> impl Iterator<Item = &'static TableEntry> {
1013    deduplicated_entries(ENTRIES.iter().chain(GENERATED_ENTRIES.iter()))
1014}
1015
1016/// Iterate both catalog projections without applying effective lookup
1017/// precedence. The semantic validator uses this to compare duplicate paths
1018/// instead of allowing `entries()` to hide stale or conflicting data.
1019pub(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/// One setting leaf before effective lookup resolves duplicated projections.
1033#[derive(Debug, Clone, Copy)]
1034pub(crate) struct ProjectedEntry {
1035    pub(crate) source: ProjectionSource,
1036    pub(crate) entry: &'static TableEntry,
1037}
1038
1039/// The source that supplied a raw settings projection entry.
1040#[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
1068/// Map the existing hero-settings leaf keys to canonical gameplay slots.
1069/// The setting tree remains the owner of the keys; display names are resolved
1070/// from the gameplay catalog by the parser/emitter when a hero context exists.
1071pub 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
1093/// Resolve an evidence-backed hero-specific setting label.
1094pub 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
1144/// Reviewed producer aliases observed in the pinned AI-PVE artifact. These
1145/// labels omit the export's `倍率` suffix or use the producer's shorter
1146/// ability label, but identify the same canonical setting path.
1147pub 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
1160/// The localized name of a game mode.
1161pub fn mode_name(key: &str) -> Option<&'static str> {
1162    name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
1163}
1164
1165/// The localized name of a map.
1166pub fn map_name(key: &str) -> Option<&'static str> {
1167    name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
1168}
1169
1170/// The localized name of a hero.
1171pub fn hero_name(key: &str) -> Option<&'static str> {
1172    name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
1173}
1174
1175/// The localized name of a team.
1176pub fn team_name(key: &str) -> Option<&'static str> {
1177    name_in(TEAM_NAMES, key)
1178}
1179
1180/// The localized name of an enum member in a domain.
1181pub 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
1194/// A human-readable rendering of a path (diagnostics).
1195pub 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}