use serde::Deserialize;
use serde_json::Value;
use std::sync::OnceLock;
const LOCALE_DATA: &str = include_str!("data/locales.json");
fn locale_data() -> &'static Value {
static DATA: OnceLock<Value> = OnceLock::new();
DATA.get_or_init(|| {
serde_json::from_str(LOCALE_DATA).expect("generated settings locale data is valid JSON")
})
}
pub fn localized_name(locale: &str, section: &str, english: &str) -> Option<&'static str> {
let data = locale_data();
let aliases = data.get(section)?.get(english)?.as_object()?;
aliases.get(locale).and_then(Value::as_str).or_else(|| {
aliases.iter().find_map(|(known, value)| {
known
.eq_ignore_ascii_case(locale)
.then(|| value.as_str())
.flatten()
})
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyKind {
Flag,
String,
Bool,
Number,
Percent,
Enum(&'static str),
ListMap,
ListHero,
}
#[derive(Debug, Clone, Copy, Hash)]
pub enum PathPart<'a> {
Part(&'a str),
Team,
Hero,
}
impl<'b> PartialEq<PathPart<'b>> for PathPart<'_> {
fn eq(&self, other: &PathPart<'b>) -> bool {
match (self, other) {
(PathPart::Part(left), PathPart::Part(right)) => left == right,
(PathPart::Team, PathPart::Team) => true,
(PathPart::Hero, PathPart::Hero) => true,
_ => false,
}
}
}
impl Eq for PathPart<'_> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableEntry {
pub path: &'static [PathPart<'static>],
pub workshop_name: &'static str,
pub kind: KeyKind,
}
macro_rules! entry {
($path:expr, $name:expr, $kind:expr) => {
TableEntry {
path: &$path,
workshop_name: $name,
kind: $kind,
}
};
}
pub static ENTRIES: &[TableEntry] = &[
entry!(
[PathPart::Part("main"), PathPart::Part("description")],
"Description",
KeyKind::String
),
entry!(
[PathPart::Part("main"), PathPart::Part("modeName")],
"Mode Name",
KeyKind::String
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("ffaSlots")],
"Max FFA Players",
KeyKind::Number
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("mapRotation")],
"Map Rotation",
KeyKind::Enum("mapRotation")
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("spectatorSlots")],
"Max Spectators",
KeyKind::Number
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("matchVoiceChat")],
"Match Voice Chat",
KeyKind::Enum("matchVoiceChat")
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("team1Slots")],
"Max Team 1 Players",
KeyKind::Number
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("team2Slots")],
"Max Team 2 Players",
KeyKind::Number
),
entry!(
[PathPart::Part("lobby"), PathPart::Part("returnToLobby")],
"Return To Lobby",
KeyKind::Enum("returnToLobby")
),
entry!(
[
PathPart::Part("lobby"),
PathPart::Part("allowPlayersInQueue")
],
"Allow Players Who Are In Queue",
KeyKind::Bool
),
entry!(
[
PathPart::Part("lobby"),
PathPart::Part("swapTeamsAfterMatch")
],
"Swap Teams After Match",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("enabled")
],
"enabled",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("control"),
PathPart::Part("enabled")
],
"enabled",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("escort"),
PathPart::Part("enabled")
],
"enabled",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("hybrid"),
PathPart::Part("enabled")
],
"enabled",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("control"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("escort"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("hybrid"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("skirmish"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("disabledMaps")
],
"disabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("skirmish"),
PathPart::Part("disabledMaps")
],
"disabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("ffa"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("tdm"),
PathPart::Part("enabledMaps")
],
"enabled maps",
KeyKind::ListMap
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("roleLimit")
],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("control"),
PathPart::Part("roleLimit")
],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("escort"),
PathPart::Part("roleLimit")
],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("hybrid"),
PathPart::Part("roleLimit")
],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("roleLimit")
],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("control"),
PathPart::Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("escort"),
PathPart::Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("hybrid"),
PathPart::Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("enablePerks")
],
"Enable Perks",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("heroLimit")
],
"Hero Limit",
KeyKind::Enum("heroLimit")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("respawnTime%")
],
"Respawn Time Scalar",
KeyKind::Percent
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("enableHeroSwitching")
],
"Allow Hero Switching",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("enableRandomHeroes")
],
"Respawn As Random Hero",
KeyKind::Bool
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("gameModeStartTrigger")
],
"Game Mode Start",
KeyKind::Enum("gameModeStartTrigger")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("gameModeStartTrigger")
],
"Game Mode Start",
KeyKind::Enum("gameModeStartTrigger")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("assault"),
PathPart::Part("tankPassiveHealthBonus")
],
"Tank Role Passive Health Bonus",
KeyKind::Enum("tankPassiveHealthBonus")
),
entry!(
[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part("spawnHealthPacks")
],
"Spawn Health Packs",
KeyKind::Enum("spawnHealthPacks")
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Part("enabledHeroes")
],
"enabled heroes",
KeyKind::ListHero
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Part("disabledHeroes")
],
"disabled heroes",
KeyKind::ListHero
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Part("general"),
PathPart::Part("disabledHeroes")
],
"disabled heroes",
KeyKind::ListHero
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("enablePrimaryFire")
],
"Primary Fire",
KeyKind::Bool
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("enableSecondaryFire")
],
"Secondary Fire",
KeyKind::Bool
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("enableAbility1")
],
"Ability 1",
KeyKind::Bool
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("enableAbility2")
],
"Ability 2",
KeyKind::Bool
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("health%")
],
"Health",
KeyKind::Percent
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("passiveUltGen%")
],
"Ultimate Generation - Passive Blizzard",
KeyKind::Percent
),
entry!(
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part("combatUltGen%")
],
"Ultimate Generation - Combat Blizzard",
KeyKind::Percent
),
];
#[derive(Debug, Clone, Copy)]
pub struct NameMap {
pub key: &'static str,
pub name: &'static str,
}
pub static MODE_NAMES: &[NameMap] = &[
NameMap {
key: "assault",
name: "Assault",
},
NameMap {
key: "control",
name: "Control",
},
NameMap {
key: "escort",
name: "Escort",
},
NameMap {
key: "hybrid",
name: "Hybrid",
},
NameMap {
key: "skirmish",
name: "Skirmish",
},
NameMap {
key: "ffa",
name: "Deathmatch",
},
NameMap {
key: "tdm",
name: "Team Deathmatch",
},
NameMap {
key: "general",
name: "General",
},
];
pub static MAP_NAMES: &[NameMap] = &[
NameMap {
key: "workshopIsland",
name: "Workshop Island",
},
NameMap {
key: "kingsRowWinter",
name: "King's Row Winter",
},
];
pub static HERO_NAMES: &[NameMap] = &[
NameMap {
key: "anran",
name: "Anran",
},
NameMap {
key: "ana",
name: "Ana",
},
NameMap {
key: "ashe",
name: "Ashe",
},
NameMap {
key: "bastion",
name: "Bastion",
},
NameMap {
key: "baptiste",
name: "Baptiste",
},
NameMap {
key: "brigitte",
name: "Brigitte",
},
NameMap {
key: "cassidy",
name: "Cassidy",
},
NameMap {
key: "dmon",
name: "D.Mon",
},
NameMap {
key: "domina",
name: "Domina",
},
NameMap {
key: "dva",
name: "D.Va",
},
NameMap {
key: "doomfist",
name: "Doomfist",
},
NameMap {
key: "echo",
name: "Echo",
},
NameMap {
key: "emre",
name: "Emre",
},
NameMap {
key: "freja",
name: "Freja",
},
NameMap {
key: "genji",
name: "Genji",
},
NameMap {
key: "hanzo",
name: "Hanzo",
},
NameMap {
key: "moira",
name: "Moira",
},
NameMap {
key: "reinhardt",
name: "Reinhardt",
},
NameMap {
key: "hammond",
name: "Wrecking Ball",
},
NameMap {
key: "hazard",
name: "Hazard",
},
NameMap {
key: "illari",
name: "Illari",
},
NameMap {
key: "juno",
name: "Juno",
},
NameMap {
key: "jetpackCat",
name: "Jetpack Cat",
},
NameMap {
key: "junkerQueen",
name: "Junker Queen",
},
NameMap {
key: "junkrat",
name: "Junkrat",
},
NameMap {
key: "kiriko",
name: "Kiriko",
},
NameMap {
key: "lucio",
name: "Lúcio",
},
NameMap {
key: "mauga",
name: "Mauga",
},
NameMap {
key: "mercy",
name: "Mercy",
},
NameMap {
key: "mizuki",
name: "Mizuki",
},
NameMap {
key: "orisa",
name: "Orisa",
},
NameMap {
key: "pharah",
name: "Pharah",
},
NameMap {
key: "reaper",
name: "Reaper",
},
NameMap {
key: "roadhog",
name: "Roadhog",
},
NameMap {
key: "shion",
name: "Shion",
},
NameMap {
key: "sierra",
name: "Sierra",
},
NameMap {
key: "sigma",
name: "Sigma",
},
NameMap {
key: "ramattra",
name: "Ramattra",
},
NameMap {
key: "lifeweaver",
name: "Lifeweaver",
},
NameMap {
key: "sojourn",
name: "Sojourn",
},
NameMap {
key: "soldier",
name: "Soldier: 76",
},
NameMap {
key: "sombra",
name: "Sombra",
},
NameMap {
key: "symmetra",
name: "Symmetra",
},
NameMap {
key: "torbjorn",
name: "Torbjörn",
},
NameMap {
key: "tracer",
name: "Tracer",
},
NameMap {
key: "venture",
name: "Venture",
},
NameMap {
key: "widowmaker",
name: "Widowmaker",
},
NameMap {
key: "winston",
name: "Winston",
},
NameMap {
key: "wuyang",
name: "Wuyang",
},
NameMap {
key: "wreckingBall",
name: "Wrecking Ball",
},
NameMap {
key: "zarya",
name: "Zarya",
},
NameMap {
key: "zenyatta",
name: "Zenyatta",
},
NameMap {
key: "mei",
name: "Mei",
},
];
include!("data/generated_map_entries.rs");
include!("data/generated_hero_entries.rs");
include!("data/generated_mode_entries.rs");
pub static TEAM_NAMES: &[NameMap] = &[
NameMap {
key: "allTeams",
name: "General",
},
NameMap {
key: "team1",
name: "Team 1",
},
NameMap {
key: "team2",
name: "Team 2",
},
];
#[derive(Debug, Clone, Copy)]
pub struct EnumMember {
pub domain: &'static str,
pub member: &'static str,
pub name: &'static str,
}
include!("data/generated_entries.rs");
include!("data/generated_hero_settings.rs");
pub static ENUM_MEMBERS: &[EnumMember] = &[
EnumMember {
domain: "mapRotation",
member: "afterAGame",
name: "After A Game",
},
EnumMember {
domain: "mapRotation",
member: "afterMirrorMatch",
name: "After A Mirror Match",
},
EnumMember {
domain: "mapRotation",
member: "paused",
name: "Paused",
},
EnumMember {
domain: "matchVoiceChat",
member: "enabled",
name: "Enabled",
},
EnumMember {
domain: "returnToLobby",
member: "never",
name: "Never",
},
EnumMember {
domain: "returnToLobby",
member: "afterAGame",
name: "After A Game",
},
EnumMember {
domain: "returnToLobby",
member: "afterMirrorMatch",
name: "After A Mirror Match",
},
EnumMember {
domain: "gameModeStartTrigger",
member: "immediately",
name: "Immediately",
},
EnumMember {
domain: "gameModeStartTrigger",
member: "manual",
name: "Manual",
},
EnumMember {
domain: "spawnHealthPacks",
member: "disabled",
name: "Disabled",
},
EnumMember {
domain: "spawnHealthPacks",
member: "modeDependent",
name: "Determined By Mode",
},
EnumMember {
domain: "spawnHealthPacks",
member: "enabled",
name: "Enabled",
},
EnumMember {
domain: "roleLimit",
member: "2OfEachRolePerTeam",
name: "2 Of Each Role Per Team",
},
EnumMember {
domain: "roleLimit",
member: "1Tank2Offense2Support",
name: "1 Tank 2 Offense 2 Support",
},
EnumMember {
domain: "roleLimit",
member: "off",
name: "Off",
},
EnumMember {
domain: "tankPassiveHealthBonus",
member: "alwaysEnabled",
name: "Always Enabled",
},
EnumMember {
domain: "tankPassiveHealthBonus",
member: "disabled",
name: "Disabled",
},
EnumMember {
domain: "heroLimit",
member: "off",
name: "Off",
},
EnumMember {
domain: "heroLimit",
member: "1PerTeam",
name: "1 Per Team",
},
EnumMember {
domain: "heroLimit",
member: "2PerTeam",
name: "2 Per Team",
},
EnumMember {
domain: "heroLimit",
member: "1PerGame",
name: "1 Per Game",
},
EnumMember {
domain: "heroLimit",
member: "2PerGame",
name: "2 Per Game",
},
];
pub fn lookup(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
entries().find(|entry| {
entry.path.len() == path.len() && entry.path.iter().zip(path.iter()).all(|(a, b)| a == b)
})
}
pub fn entries() -> impl Iterator<Item = &'static TableEntry> {
deduplicated_entries(ENTRIES.iter().chain(GENERATED_ENTRIES.iter()))
}
pub(crate) fn raw_entries() -> impl Iterator<Item = ProjectedEntry> {
ENTRIES
.iter()
.map(|entry| ProjectedEntry {
source: ProjectionSource::FixtureTable,
entry,
})
.chain(GENERATED_ENTRIES.iter().map(|entry| ProjectedEntry {
source: ProjectionSource::WorkshopDataExport,
entry,
}))
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ProjectedEntry {
pub(crate) source: ProjectionSource,
pub(crate) entry: &'static TableEntry,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ProjectionSource {
FixtureTable,
WorkshopDataExport,
}
impl ProjectionSource {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::FixtureTable => "fixture table",
Self::WorkshopDataExport => "Workshop-data export",
}
}
}
fn deduplicated_entries(
entries: impl Iterator<Item = &'static TableEntry>,
) -> impl Iterator<Item = &'static TableEntry> {
let mut paths = std::collections::HashSet::new();
entries.filter(move |entry| paths.insert(path_string(entry.path)))
}
pub(crate) fn is_generated_entry(entry: &TableEntry) -> bool {
GENERATED_ENTRIES
.iter()
.any(|candidate| std::ptr::eq(candidate, entry))
}
pub fn ability_slot_for_path(path: &[PathPart<'_>]) -> Option<&'static str> {
match path.last() {
Some(PathPart::Part("ability1Cooldown%" | "enableAbility1")) => Some("ability1"),
Some(PathPart::Part("ability2Cooldown%" | "enableAbility2")) => Some("ability2"),
Some(PathPart::Part("ability3Cooldown%" | "enableAbility3")) => Some("ability3"),
Some(PathPart::Part(
"secondaryFireCooldown%"
| "secondaryFireEnergyChargeRate%"
| "secondaryFireMaximumTime%"
| "secondaryFireRechargeRate%"
| "enableSecondaryFire"
| "enableGenericSecondaryFire",
)) => Some("secondaryFire"),
Some(PathPart::Part("combatUltGen%" | "passiveUltGen%" | "ultGen%" | "enableUlt")) => {
Some("ultimate")
}
Some(PathPart::Part("enablePassive")) => Some("passive"),
Some(PathPart::Part("enableAutomaticFire" | "enableScoping")) => Some("primaryFire"),
_ => None,
}
}
pub fn hero_setting_name(hero: &str, key: &str, locale: &str) -> Option<&'static str> {
let generated = GENERATED_HERO_SETTING_NAMES
.iter()
.find(|entry| entry.hero == hero && entry.key == key)
.and_then(|entry| entry.localized(locale));
generated
.or_else(|| {
hero_setting_aliases()
.iter()
.find(|alias| {
alias.hero == hero
&& alias.key == key
&& alias.locale.eq_ignore_ascii_case(locale)
})
.map(|alias| alias.display.as_str())
})
.or_else(|| {
if locale.eq_ignore_ascii_case("en-US") {
GENERATED_HERO_SETTING_NAMES
.iter()
.find(|entry| entry.hero == hero && entry.key == key)
.filter(|entry| {
entry.locales.iter().any(|(known, value)| {
known.eq_ignore_ascii_case(locale)
&& (value.trim().is_empty() || value.starts_with(' '))
})
})
.map(|entry| entry.key)
} else {
None
}
})
}
#[derive(Deserialize)]
struct HeroSettingAlias {
hero: String,
key: String,
locale: String,
display: String,
}
fn hero_setting_aliases() -> &'static [HeroSettingAlias] {
static ALIASES: OnceLock<Vec<HeroSettingAlias>> = OnceLock::new();
ALIASES.get_or_init(|| {
serde_json::from_str(include_str!("data/hero_setting_aliases.json"))
.expect("hero setting alias data is valid JSON")
})
}
pub fn hero_setting_alias(hero: &str, key: &str, locale: &str, display: &str) -> bool {
hero_setting_aliases().iter().any(|alias| {
alias.hero == hero
&& alias.key == key
&& alias.locale.eq_ignore_ascii_case(locale)
&& alias.display == display
})
}
fn name_in(maps: &[NameMap], key: &str) -> Option<&'static str> {
maps.iter().find(|m| m.key == key).map(|m| m.name)
}
pub fn mode_name(key: &str) -> Option<&'static str> {
name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
}
pub fn map_name(key: &str) -> Option<&'static str> {
name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
}
pub fn hero_name(key: &str) -> Option<&'static str> {
name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
}
pub fn team_name(key: &str) -> Option<&'static str> {
name_in(TEAM_NAMES, key)
}
pub fn enum_name(domain: &str, member: &str) -> Option<&'static str> {
ENUM_MEMBERS
.iter()
.find(|m| m.domain == domain && m.member == member)
.map(|m| m.name)
.or_else(|| {
GENERATED_ENUM_MEMBERS
.iter()
.find(|m| m.domain == domain && m.member == member)
.map(|m| m.name)
})
}
pub fn path_string(path: &[PathPart<'_>]) -> String {
path.iter()
.map(|part| match part {
PathPart::Part(name) => (*name).to_string(),
PathPart::Team => "<team>".to_string(),
PathPart::Hero => "<hero>".to_string(),
})
.collect::<Vec<_>>()
.join(".")
}