use serde::Deserialize;
use serde_json::Value;
use std::sync::OnceLock;
use super::PathPart::{self, Hero, Part, Team};
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(crate) 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 struct LocalizedSettingName {
pub section: &'static str,
pub english: &'static str,
pub locale: &'static str,
pub spelling: &'static str,
}
const LOCALE_SECTIONS: &[&str] = &[
"enums",
"heroes",
"labels",
"maps",
"modes",
"namespaces",
"teams",
"tokens",
];
pub fn localized_setting_names() -> &'static [LocalizedSettingName] {
static NAMES: OnceLock<Vec<LocalizedSettingName>> = OnceLock::new();
NAMES.get_or_init(|| {
let data = locale_data();
let mut names = Vec::new();
for section in LOCALE_SECTIONS {
let Some(rows) = data.get(section).and_then(Value::as_object) else {
continue;
};
for (english, row) in rows {
let Some(aliases) = row.as_object() else {
continue;
};
for (locale, value) in aliases {
let Some(spelling) = value.as_str() else {
continue;
};
names.push(LocalizedSettingName {
section,
english,
locale,
spelling,
});
}
}
}
names
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum KeyKind {
Flag,
String,
Bool,
YesNo,
BoolEnum(&'static str),
Number,
Percent,
Enum(&'static str),
ListMap,
ListHero,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TableEntry {
pub(crate) path: &'static [PathPart<'static>],
pub(crate) workshop_name: &'static str,
pub(crate) kind: KeyKind,
}
macro_rules! entry {
($path:expr, $name:expr, $kind:expr) => {
TableEntry {
path: &$path,
workshop_name: $name,
kind: $kind,
}
};
}
pub(crate) static ENTRIES: &[TableEntry] = &[
entry!(
[Part("main"), Part("description")],
"Description",
KeyKind::String
),
entry!(
[Part("main"), Part("modeName")],
"Mode Name",
KeyKind::String
),
entry!(
[Part("lobby"), Part("ffaSlots")],
"Max FFA Players",
KeyKind::Number
),
entry!(
[Part("lobby"), Part("mapRotation")],
"Map Rotation",
KeyKind::Enum("mapRotation")
),
entry!(
[Part("lobby"), Part("spectatorSlots")],
"Max Spectators",
KeyKind::Number
),
entry!(
[Part("lobby"), Part("enableMatchVoiceChat")],
"Match Voice Chat",
KeyKind::BoolEnum("matchVoiceChat")
),
entry!(
[Part("lobby"), Part("team1Slots")],
"Max Team 1 Players",
KeyKind::Number
),
entry!(
[Part("lobby"), Part("team2Slots")],
"Max Team 2 Players",
KeyKind::Number
),
entry!(
[Part("lobby"), Part("returnToLobby")],
"Return To Lobby",
KeyKind::Enum("returnToLobby")
),
entry!(
[Part("lobby"), Part("allowPlayersInQueue")],
"Allow Players Who Are In Queue",
KeyKind::YesNo
),
entry!(
[Part("lobby"), Part("swapTeamsAfterMatch")],
"Swap Teams After Match",
KeyKind::YesNo
),
entry!(
[Part("gamemodes"), Part("assault"), Part("enabled")],
"enabled",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("control"), Part("enabled")],
"enabled",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("escort"), Part("enabled")],
"enabled",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("hybrid"), Part("enabled")],
"enabled",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("assault"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("control"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("escort"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("hybrid"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("skirmish"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("assault"), Part("disabledMaps")],
"disabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("skirmish"), Part("disabledMaps")],
"disabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("ffa"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("tdm"), Part("enabledMaps")],
"enabled maps",
KeyKind::ListMap
),
entry!(
[Part("gamemodes"), Part("assault"), Part("roleLimit")],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[Part("gamemodes"), Part("control"), Part("roleLimit")],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[Part("gamemodes"), Part("escort"), Part("roleLimit")],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[Part("gamemodes"), Part("hybrid"), Part("roleLimit")],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[Part("gamemodes"), Part("general"), Part("roleLimit")],
"Limit Roles",
KeyKind::Enum("roleLimit")
),
entry!(
[
Part("gamemodes"),
Part("assault"),
Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("control"),
Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("escort"),
Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("hybrid"),
Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("general"),
Part("enableCompetitiveRules")
],
"Competitive Rules",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("general"), Part("enablePerks")],
"Enable Perks",
KeyKind::Bool
),
entry!(
[Part("gamemodes"), Part("general"), Part("heroLimit")],
"Hero Limit",
KeyKind::Enum("heroLimit")
),
entry!(
[Part("gamemodes"), Part("general"), Part("respawnTime%")],
"Respawn Time Scalar",
KeyKind::Percent
),
entry!(
[
Part("gamemodes"),
Part("general"),
Part("enableHeroSwitching")
],
"Allow Hero Switching",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("general"),
Part("enableRandomHeroes")
],
"Respawn As Random Hero",
KeyKind::Bool
),
entry!(
[
Part("gamemodes"),
Part("general"),
Part("gamemodeStartTrigger")
],
"Game Mode Start",
KeyKind::Enum("gamemodeStartTrigger")
),
entry!(
[
Part("gamemodes"),
Part("assault"),
Part("gamemodeStartTrigger")
],
"Game Mode Start",
KeyKind::Enum("gamemodeStartTrigger")
),
entry!(
[
Part("gamemodes"),
Part("assault"),
Part("tankPassiveHealthBonus")
],
"Tank Role Passive Health Bonus",
KeyKind::Enum("tankPassiveHealthBonus")
),
entry!(
[Part("gamemodes"), Part("general"), Part("spawnHealthPacks")],
"Spawn Health Packs",
KeyKind::Enum("spawnHealthPacks")
),
entry!(
[Part("heroes"), Team, Part("enabledHeroes")],
"enabled heroes",
KeyKind::ListHero
),
entry!(
[Part("heroes"), Team, Part("disabledHeroes")],
"disabled heroes",
KeyKind::ListHero
),
entry!(
[Part("heroes"), Part("general"), Part("disabledHeroes")],
"disabled heroes",
KeyKind::ListHero
),
entry!(
[Part("heroes"), Team, Hero, Part("enablePrimaryFire")],
"Primary Fire",
KeyKind::Bool
),
entry!(
[Part("heroes"), Team, Hero, Part("enableSecondaryFire")],
"Secondary Fire",
KeyKind::Bool
),
entry!(
[Part("heroes"), Team, Hero, Part("enableAbility1")],
"Ability 1",
KeyKind::Bool
),
entry!(
[Part("heroes"), Team, Hero, Part("enableAbility2")],
"Ability 2",
KeyKind::Bool
),
entry!(
[Part("heroes"), Team, Hero, Part("health%")],
"Health",
KeyKind::Percent
),
entry!(
[Part("heroes"), Team, Hero, Part("passiveUltGen%")],
"Ultimate Generation - Passive Blizzard",
KeyKind::Percent
),
entry!(
[Part("heroes"), Team, Hero, Part("combatUltGen%")],
"Ultimate Generation - Combat Blizzard",
KeyKind::Percent
),
];
#[derive(Debug, Clone, Copy)]
pub(crate) struct NameMap {
pub(crate) key: &'static str,
pub(crate) name: &'static str,
}
const fn named(key: &'static str, name: &'static str) -> NameMap {
NameMap { key, name }
}
pub(crate) static MODE_NAMES: &[NameMap] = &[
named("assault", "Assault"),
named("control", "Control"),
named("escort", "Escort"),
named("hybrid", "Hybrid"),
named("skirmish", "Skirmish"),
named("ffa", "Deathmatch"),
named("tdm", "Team Deathmatch"),
named("general", "General"),
];
pub(crate) static MAP_NAMES: &[NameMap] = &[
named("workshopIsland", "Workshop Island"),
named("kingsRowWinter", "King's Row Winter"),
];
pub(crate) static HERO_NAMES: &[NameMap] = &[
named("anran", "Anran"),
named("ana", "Ana"),
named("ashe", "Ashe"),
named("bastion", "Bastion"),
named("baptiste", "Baptiste"),
named("brigitte", "Brigitte"),
named("cassidy", "Cassidy"),
named("dmon", "D.Mon"),
named("domina", "Domina"),
named("dva", "D.Va"),
named("doomfist", "Doomfist"),
named("echo", "Echo"),
named("emre", "Emre"),
named("freja", "Freja"),
named("genji", "Genji"),
named("hanzo", "Hanzo"),
named("moira", "Moira"),
named("reinhardt", "Reinhardt"),
named("hazard", "Hazard"),
named("illari", "Illari"),
named("juno", "Juno"),
named("jetpackCat", "Jetpack Cat"),
named("junkerQueen", "Junker Queen"),
named("junkrat", "Junkrat"),
named("kiriko", "Kiriko"),
named("lucio", "Lúcio"),
named("mauga", "Mauga"),
named("mercy", "Mercy"),
named("mizuki", "Mizuki"),
named("orisa", "Orisa"),
named("pharah", "Pharah"),
named("reaper", "Reaper"),
named("roadhog", "Roadhog"),
named("shion", "Shion"),
named("sierra", "Sierra"),
named("sigma", "Sigma"),
named("ramattra", "Ramattra"),
named("lifeweaver", "Lifeweaver"),
named("sojourn", "Sojourn"),
named("soldier", "Soldier: 76"),
named("sombra", "Sombra"),
named("symmetra", "Symmetra"),
named("torbjorn", "Torbjörn"),
named("tracer", "Tracer"),
named("venture", "Venture"),
named("widowmaker", "Widowmaker"),
named("winston", "Winston"),
named("wuyang", "Wuyang"),
named("wreckingBall", "Wrecking Ball"),
named("zarya", "Zarya"),
named("zenyatta", "Zenyatta"),
named("mei", "Mei"),
];
include!("data/generated_map_entries.rs");
include!("data/generated_hero_entries.rs");
include!("data/generated_mode_entries.rs");
pub(crate) static TEAM_NAMES: &[NameMap] = &[
named("allTeams", "General"),
named("team1", "Team 1"),
named("team2", "Team 2"),
];
#[derive(Debug, Clone, Copy)]
pub(crate) struct EnumMember {
pub(crate) domain: &'static str,
pub(crate) member: &'static str,
pub(crate) name: &'static str,
}
const fn enum_member(domain: &'static str, member: &'static str, name: &'static str) -> EnumMember {
EnumMember {
domain,
member,
name,
}
}
include!("data/generated_entries.rs");
include!("data/generated_hero_settings.rs");
pub(crate) static ENUM_MEMBERS: &[EnumMember] = &[
enum_member("mapRotation", "afterAGame", "After A Game"),
enum_member("mapRotation", "afterMirrorMatch", "After A Mirror Match"),
enum_member("mapRotation", "paused", "Paused"),
enum_member("matchVoiceChat", "enabled", "Enabled"),
enum_member("returnToLobby", "never", "Never"),
enum_member("returnToLobby", "afterAGame", "After A Game"),
enum_member("returnToLobby", "afterMirrorMatch", "After A Mirror Match"),
enum_member("gamemodeStartTrigger", "immediately", "Immediately"),
enum_member("gamemodeStartTrigger", "manual", "Manual"),
enum_member("gamemodeStartTrigger", "allSlotsFilled", "All Slots Filled"),
enum_member("spawnHealthPacks", "disabled", "Disabled"),
enum_member("spawnHealthPacks", "modeDependent", "Determined By Mode"),
enum_member("spawnHealthPacks", "enabled", "Enabled"),
enum_member("roleLimit", "2OfEachRolePerTeam", "2 Of Each Role Per Team"),
enum_member(
"roleLimit",
"1Tank2Offense2Support",
"1 Tank 2 Offense 2 Support",
),
enum_member("roleLimit", "off", "Off"),
enum_member("tankPassiveHealthBonus", "alwaysEnabled", "Always Enabled"),
enum_member("tankPassiveHealthBonus", "disabled", "Disabled"),
enum_member(
"tankPassiveHealthBonus",
"1Tank2Offense2Support",
"1 Tank 2 Offense 2 Support",
),
enum_member("heroLimit", "off", "Off"),
enum_member("heroLimit", "1PerTeam", "1 Per Team"),
enum_member("heroLimit", "2PerTeam", "2 Per Team"),
enum_member("heroLimit", "1PerGame", "1 Per Game"),
enum_member("heroLimit", "2PerGame", "2 Per Game"),
];
pub(crate) fn lookup(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
exact_entry(path).or_else(|| mode_inherited_entry(path))
}
fn exact_entry(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
entry_bucket(path).find(|entry| entry.path == path)
}
fn entry_bucket(path: &[PathPart<'_>]) -> impl Iterator<Item = &'static TableEntry> {
static INDEX: OnceLock<std::collections::HashMap<u64, Vec<&'static TableEntry>>> =
OnceLock::new();
let index = INDEX.get_or_init(|| {
let mut index: std::collections::HashMap<u64, Vec<&'static TableEntry>> =
std::collections::HashMap::new();
for entry in entries() {
index.entry(path_hash(entry.path)).or_default().push(entry);
}
index
});
index
.get(&path_hash(path))
.into_iter()
.flat_map(|bucket| bucket.iter().copied())
}
fn path_hash(path: &[PathPart<'_>]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
}
fn declared_modes() -> &'static std::collections::HashSet<&'static str> {
static MODES: OnceLock<std::collections::HashSet<&'static str>> = OnceLock::new();
MODES.get_or_init(|| {
entries()
.filter_map(|entry| {
let [PathPart::Part("gamemodes"), PathPart::Part(mode), ..] = entry.path else {
return None;
};
Some(*mode)
})
.collect()
})
}
pub(crate) fn child_entries(parent: &[PathPart<'_>]) -> Vec<&'static TableEntry> {
static CHILDREN: OnceLock<std::collections::HashMap<u64, Vec<(usize, &'static TableEntry)>>> =
OnceLock::new();
let children = CHILDREN.get_or_init(|| {
let mut children: std::collections::HashMap<u64, Vec<(usize, &'static TableEntry)>> =
std::collections::HashMap::new();
for entry in entries() {
if let Some((_, parent)) = entry.path.split_last() {
children
.entry(path_hash(parent))
.or_default()
.push((entry.path.len() - 1, entry));
}
}
children
});
children
.get(&path_hash(parent))
.map(|bucket| {
bucket
.iter()
.filter(|(parent_len, entry)| {
*parent_len == parent.len() && entry.path[..parent.len()] == *parent
})
.map(|(_, entry)| *entry)
.collect()
})
.unwrap_or_default()
}
static ELIMINATION_GENERAL_KEYS: &[&str] = &[
"disabledMaps",
"enableEnemyHealthBars",
"enableKillCam",
"enableKillFeed",
"enableSkins",
"enabledMaps",
"gamemodeStartTrigger",
"healthPackRespawnTime%",
"perkEliminationCatchupLevelAmount%",
"perkGeneration%",
"spawnHealthPacks",
"teamOverlay",
];
pub(crate) fn mode_inherited_entry(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
let [
PathPart::Part("gamemodes"),
PathPart::Part(mode),
PathPart::Part(key),
] = path
else {
return None;
};
if *mode == "general" {
return None;
}
let inherits = if *mode == "elimination" {
ELIMINATION_GENERAL_KEYS.contains(key)
} else {
declared_modes().contains(mode)
};
if !inherits {
return None;
}
exact_entry(&[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part(key),
])
}
pub(crate) 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(crate) 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"),
_ => None,
}
}
pub(crate) fn hero_setting_name(hero: &str, key: &str, locale: &str) -> Option<&'static str> {
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(|| {
GENERATED_HERO_SETTING_NAMES
.iter()
.find(|entry| entry.hero == hero && entry.key == key)
.and_then(|entry| entry.localized(locale))
})
.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
}
})
}
pub(crate) fn hero_setting_display_matches(
hero: &str,
key: &str,
locale: &str,
display: &str,
) -> bool {
if hero_setting_has_alias(hero, key, locale) {
return false;
}
GENERATED_HERO_SETTING_NAMES
.iter()
.filter(|entry| entry.hero == hero && entry.key == key)
.filter_map(|entry| entry.localized(locale))
.any(|name| names_eq(name, display))
}
pub(crate) fn hero_key_template(key: &str) -> Option<&'static str> {
GENERATED_ENTRIES
.iter()
.find(|entry| {
matches!(
entry.path,
[
PathPart::Part("heroes"),
PathPart::Team,
PathPart::Hero,
PathPart::Part(candidate)
] if *candidate == key
)
})
.map(|entry| entry.workshop_name)
.filter(|name| name.contains("%1$s"))
}
#[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(crate) fn hero_setting_has_alias(hero: &str, key: &str, locale: &str) -> bool {
hero_setting_aliases().iter().any(|alias| {
alias.hero == hero && alias.key == key && alias.locale.eq_ignore_ascii_case(locale)
})
}
pub(crate) 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)
&& names_eq(&alias.display, display)
})
}
const BOOL_VALUE_ALIASES: &[(&str, &str, &str, bool)] =
&[("enableSecondaryFire", "zh-CN", "禁用", false)];
pub(crate) fn bool_value_alias(key: &str, locale: &str, display: &str) -> Option<bool> {
BOOL_VALUE_ALIASES
.iter()
.find(|(alias_key, alias_locale, spelling, _)| {
*alias_key == key
&& alias_locale.eq_ignore_ascii_case(locale)
&& names_eq(spelling, display)
})
.map(|(_, _, _, value)| *value)
}
fn name_in(maps: &[NameMap], key: &str) -> Option<&'static str> {
maps.iter().find(|m| m.key == key).map(|m| m.name)
}
pub(crate) fn names_eq(left: &str, right: &str) -> bool {
left == right
|| left.to_lowercase() == right.to_lowercase()
|| crate::catalog::normalize_spelling(left).to_lowercase()
== crate::catalog::normalize_spelling(right).to_lowercase()
}
pub(crate) fn map_spellings() -> impl Iterator<Item = &'static str> {
MAP_NAMES
.iter()
.chain(GENERATED_MAP_NAMES.iter())
.map(|m| m.name)
}
pub(crate) fn hero_spellings() -> impl Iterator<Item = &'static str> {
HERO_NAMES
.iter()
.chain(GENERATED_HERO_NAMES.iter())
.map(|m| m.name)
}
pub(crate) fn team_spellings() -> impl Iterator<Item = &'static str> {
TEAM_NAMES.iter().map(|m| m.name)
}
pub(crate) fn enum_spellings(domain: &str) -> impl Iterator<Item = &'static str> {
enum_members(domain).map(|member| member.name)
}
pub(crate) fn key_spellings(parent: &[PathPart<'_>]) -> Vec<&'static str> {
let mut spellings: Vec<&'static str> = child_entries(parent)
.iter()
.filter(|entry| {
!entry.workshop_name.contains("%1$s")
})
.map(|entry| entry.workshop_name)
.collect();
let [PathPart::Part("gamemodes"), PathPart::Part(mode)] = parent else {
return spellings;
};
if *mode == "general" {
return spellings;
}
if *mode == "elimination" {
spellings.extend(
child_entries(&[PathPart::Part("gamemodes"), PathPart::Part("general")])
.iter()
.filter_map(|entry| {
let PathPart::Part(key) = entry.path.last()? else {
return None;
};
ELIMINATION_GENERAL_KEYS
.contains(key)
.then_some(entry.workshop_name)
}),
);
} else if declared_modes().contains(mode) {
spellings.extend(
child_entries(&[PathPart::Part("gamemodes"), PathPart::Part("general")])
.iter()
.filter_map(|entry| {
(!entry.workshop_name.contains("%1$s")).then_some(entry.workshop_name)
}),
);
}
spellings
}
pub(crate) fn mode_name(key: &str) -> Option<&'static str> {
name_in(MODE_NAMES, key).or_else(|| name_in(GENERATED_MODE_NAMES, key))
}
pub(crate) fn map_name(key: &str) -> Option<&'static str> {
name_in(MAP_NAMES, key).or_else(|| name_in(GENERATED_MAP_NAMES, key))
}
pub(crate) fn hero_name(key: &str) -> Option<&'static str> {
name_in(HERO_NAMES, key).or_else(|| name_in(GENERATED_HERO_NAMES, key))
}
pub(crate) fn team_name(key: &str) -> Option<&'static str> {
name_in(TEAM_NAMES, key)
}
pub(crate) 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(crate) fn enum_members(domain: &str) -> impl Iterator<Item = &'static EnumMember> {
ENUM_MEMBERS
.iter()
.chain(GENERATED_ENUM_MEMBERS.iter())
.filter(move |member| member.domain == domain)
}
pub(crate) 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(".")
}
#[cfg(test)]
mod tests {
use super::*;
fn scan_exact(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
entries().find(|entry| entry.path == path)
}
fn scan_mode_inherited(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
let [
PathPart::Part("gamemodes"),
PathPart::Part(mode),
PathPart::Part(key),
] = path
else {
return None;
};
if *mode == "general" {
return None;
}
let inherits = if *mode == "elimination" {
ELIMINATION_GENERAL_KEYS.contains(key)
} else {
entries().any(|entry| {
matches!(
entry.path,
[PathPart::Part("gamemodes"), PathPart::Part(declared), ..]
if *declared == *mode
)
})
};
if !inherits {
return None;
}
scan_exact(&[
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part(key),
])
}
fn scan_lookup(path: &[PathPart<'_>]) -> Option<&'static TableEntry> {
scan_exact(path).or_else(|| scan_mode_inherited(path))
}
#[test]
fn index_matches_scan_for_every_table_path() {
let mut checked = 0usize;
for entry in entries() {
assert_eq!(
scan_lookup(entry.path).map(std::ptr::from_ref),
lookup(entry.path).map(std::ptr::from_ref),
"path {}",
path_string(entry.path),
);
checked += 1;
if let [
PathPart::Part("gamemodes"),
PathPart::Part(_),
PathPart::Part(key),
] = entry.path
{
for mode in declared_modes().iter().copied().chain(["elimination"]) {
let path = [
PathPart::Part("gamemodes"),
PathPart::Part(mode),
PathPart::Part(key),
];
assert_eq!(
scan_lookup(&path).map(std::ptr::from_ref),
lookup(&path).map(std::ptr::from_ref),
"path {}",
path_string(&path),
);
checked += 1;
}
}
}
let non_resolving: &[&[PathPart<'_>]] = &[
&[],
&[PathPart::Part("main")],
&[PathPart::Part("lobby")],
&[PathPart::Part("unknownNamespace"), PathPart::Part("key")],
&[PathPart::Part("lobby"), PathPart::Part("unknownKey")],
&[PathPart::Part("gamemodes"), PathPart::Part("general")],
&[
PathPart::Part("gamemodes"),
PathPart::Part("undeclaredMode"),
PathPart::Part("enabledMaps"),
],
&[
PathPart::Part("gamemodes"),
PathPart::Part("elimination"),
PathPart::Part("healthPackRespawnTime"),
],
&[PathPart::Part("heroes"), PathPart::Team],
];
for path in non_resolving {
assert_eq!(
scan_lookup(path).map(std::ptr::from_ref),
lookup(path).map(std::ptr::from_ref),
"path {}",
path_string(path),
);
checked += 1;
}
for entry in entries() {
let mismatched: Vec<PathPart<'_>> = entry
.path
.iter()
.map(|part| match part {
PathPart::Part(_) | PathPart::Team => PathPart::Hero,
PathPart::Hero => PathPart::Team,
})
.collect();
if mismatched == *entry.path {
continue;
}
assert_eq!(
scan_lookup(&mismatched).map(std::ptr::from_ref),
lookup(&mismatched).map(std::ptr::from_ref),
"path {}",
path_string(&mismatched),
);
checked += 1;
}
assert!(checked > 500, "derived coverage was thin: {checked} paths");
}
#[test]
fn key_spellings_match_scan_for_every_table_parent() {
fn scan_key_spellings(parent: &[PathPart<'_>]) -> Vec<&'static str> {
let mut spellings: Vec<&'static str> = entries()
.filter(|entry| {
entry.path.len() == parent.len() + 1
&& entry.path[..parent.len()] == *parent
&& !entry.workshop_name.contains("%1$s")
})
.map(|entry| entry.workshop_name)
.collect();
let [PathPart::Part("gamemodes"), PathPart::Part(mode)] = parent else {
return spellings;
};
if *mode == "general" {
return spellings;
}
if *mode == "elimination" {
spellings.extend(entries().filter_map(|entry| {
let [
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part(key),
] = entry.path
else {
return None;
};
ELIMINATION_GENERAL_KEYS
.contains(key)
.then_some(entry.workshop_name)
}));
} else if entries().any(|entry| {
matches!(
entry.path,
[PathPart::Part("gamemodes"), PathPart::Part(declared), ..]
if *declared == *mode
)
}) {
spellings.extend(entries().filter_map(|entry| {
let [
PathPart::Part("gamemodes"),
PathPart::Part("general"),
PathPart::Part(_),
] = entry.path
else {
return None;
};
(!entry.workshop_name.contains("%1$s")).then_some(entry.workshop_name)
}));
}
spellings
}
let mut parents: Vec<Vec<PathPart<'_>>> = Vec::new();
for entry in entries() {
for len in 0..entry.path.len() {
let parent = entry.path[..len].to_vec();
if !parents.contains(&parent) {
parents.push(parent);
}
}
}
parents.push(vec![PathPart::Part("unknownNamespace")]);
parents.push(vec![
PathPart::Part("gamemodes"),
PathPart::Part("undeclaredMode"),
]);
for parent in &parents {
assert_eq!(
scan_key_spellings(parent),
key_spellings(parent),
"parent {}",
path_string(parent),
);
}
}
}