use super::member::{self, Member};
use crate::core::suggest;
use crate::output::emitter::*;
use crate::source::Span;
impl EmitContext<'_> {
pub(crate) fn emit_settings(&mut self, settings: &SettingsTree) -> Result<()> {
let settings_keyword = self.structural("settings")?;
self.line(0, &format!("{settings_keyword} {{"))?;
for child in &settings.children {
if let SettingsNode::Workshop { children, .. } = child {
self.emit_workshop_settings(children, 1)?;
continue;
}
let SettingsNode::Group { name, children, .. } = child else {
return Err(WorkshopError::malformed(
"settings block children must be groups",
child.span(),
));
};
match name.as_str() {
"main" | "lobby" => {
let display =
self.setting_name("namespaces", name, &format!("namespace.{name}"))?;
self.line(1, &format!("{display} {{"))?;
for member in children {
self.settings_member(member, 2, &[PathPart::Part(name)], None)?;
}
self.line(1, "}")?;
}
"gamemodes" => self.emit_modes(children)?,
"heroes" => self.emit_heroes(children)?,
"extensions" => {
let display =
self.setting_name("namespaces", "extensions", "namespace.extensions")?;
self.line(1, &format!("{display} {{"))?;
for member in children {
self.settings_member(member, 2, &[PathPart::Part("extensions")], None)?;
}
self.line(1, "}")?;
}
_ => self.emit_opaque_group(children, name, 1)?,
}
}
self.line(0, "}")?;
Ok(())
}
pub(crate) fn emit_workshop_settings(
&mut self,
children: &[SettingsNode],
level: usize,
) -> Result<()> {
let workshop = self.setting_name("namespaces", "workshop", "namespace.workshop")?;
self.line(level, &format!("{workshop} {{"))?;
for child in children {
self.emit_workshop_node(child, level + 1)?;
}
self.line(level, "}")?;
Ok(())
}
pub(crate) fn emit_workshop_node(&mut self, node: &SettingsNode, level: usize) -> Result<()> {
match node {
SettingsNode::Group { name, children, .. } => {
self.line(level, &format!("{name} {{"))?;
for child in children {
self.emit_workshop_node(child, level + 1)?;
}
self.line(level, "}")?;
Ok(())
}
SettingsNode::Workshop { children, .. } => self.emit_workshop_settings(children, level),
SettingsNode::Raw { name, value, .. } => {
if value.is_empty() {
self.line(level, name)
} else {
self.line(level, &format!("{name}: {value}"))
}
}
_ => Err(WorkshopError::malformed(
"settings.workshop contains a typed builtin setting",
node.span(),
)),
}
}
pub(crate) fn emit_modes(&mut self, modes: &[SettingsNode]) -> Result<()> {
let modes_keyword = self.setting_name("namespaces", "modes", "namespace.modes")?;
self.line(1, &format!("{modes_keyword} {{"))?;
for mode in modes {
let SettingsNode::Group { name, children, .. } = mode else {
return Err(WorkshopError::malformed(
"mode entries must be groups",
mode.span(),
));
};
let display = match table::mode_name(name) {
Some(english) => self.setting_name("modes", english, &format!("mode.{name}"))?,
None => name.clone(),
};
let disabled = children.iter().any(|member| {
matches!(
member,
SettingsNode::Bool { name: n, value: false, .. } if n == "enabled"
)
});
let header = if disabled {
let disabled_name = self.setting_name("tokens", "disabled", "token.disabled")?;
format!("{disabled_name} {display}")
} else {
display
};
self.line(2, &format!("{header} {{"))?;
for member in children {
if matches!(member, SettingsNode::Bool { name: n, .. } if n == "enabled") {
continue;
}
self.settings_member(
member,
3,
&[PathPart::Part("gamemodes"), PathPart::Part(name)],
None,
)?;
}
self.line(2, "}")?;
}
self.line(1, "}")?;
Ok(())
}
pub(crate) fn emit_heroes(&mut self, teams: &[SettingsNode]) -> Result<()> {
let heroes_keyword = self.setting_name("namespaces", "heroes", "namespace.heroes")?;
self.line(1, &format!("{heroes_keyword} {{"))?;
for team in teams {
let SettingsNode::Group { name, children, .. } = team else {
return Err(WorkshopError::malformed(
"team entries must be groups",
team.span(),
));
};
let english = table::team_name(name).ok_or_else(|| {
suggested(
format!("unknown team '{name}'"),
suggest::suggest(name, table::team_spellings()),
team.span(),
)
})?;
let display = self.setting_name("teams", english, &format!("team.{name}"))?;
self.line(2, &format!("{display} {{"))?;
for member in children {
match member {
SettingsNode::Group { name, children, .. } => {
let english = table::hero_name(name).ok_or_else(|| {
suggested(
format!("unknown hero '{name}'"),
suggest::suggest(name, table::hero_spellings()),
member.span(),
)
})?;
let hero = self.setting_name("heroes", english, &format!("hero.{name}"))?;
self.line(3, &format!("{hero} {{"))?;
for inner in children {
self.settings_member(
inner,
4,
&[PathPart::Part("heroes"), PathPart::Team, PathPart::Hero],
Some(name),
)?;
}
self.line(3, "}")?;
}
other => self.settings_member(
other,
3,
&[PathPart::Part("heroes"), PathPart::Team],
None,
)?,
}
}
self.line(2, "}")?;
}
self.line(1, "}")?;
Ok(())
}
pub(crate) fn settings_member(
&mut self,
node: &SettingsNode,
level: usize,
path: &[PathPart],
hero: Option<&str>,
) -> Result<()> {
if let SettingsNode::Raw { name, value, .. } = node {
if value.is_empty() {
self.line(level, name)?;
} else {
self.line(level, &format!("{name}: {value}"))?;
}
return Ok(());
}
let name = node.name();
let mut full = path.to_vec();
full.push(PathPart::Part(name));
let entry = member::lookup(node, &full).map_err(|diagnostic| diagnostic.error)?;
let display_name = if let (Some(hero), Some(key)) = (
hero,
full.last().and_then(|part| match part {
PathPart::Part(key) => Some(*key),
_ => None,
}),
) {
if let Some(name) = table::hero_setting_name(hero, key, self.locale.as_str()) {
name.to_string()
} else if !matches!(
key,
"enableAbility1" | "enableAbility2" | "enableAbility3" | "enableSecondaryFire"
) {
if let Some(slot) = table::ability_slot_for_path(&full) {
self.gameplay_setting_name(hero, slot, &table::path_string(&full))?
} else {
self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
}
} else {
self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
}
} else if let (Some(hero), Some(slot)) = (hero, table::ability_slot_for_path(&full)) {
self.gameplay_setting_name(hero, slot, &table::path_string(&full))?
} else {
self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
};
match member::accept(node, entry).map_err(|diagnostic| diagnostic.error)? {
Member::Flag => self.line(level, &display_name)?,
Member::String(value) => self.line(
level,
&format!("{}: \"{}\"", display_name, escape_settings_string(value)),
)?,
Member::Enum {
domain,
value,
english,
} => {
let display =
self.setting_name("enums", english, &format!("enum.{domain}.{value}"))?;
self.line(level, &format!("{display_name}: {display}"))?;
}
Member::Number(value) => self.line(
level,
&format!(
"{display_name}: {}",
crate::format::format_setting_number(value)
),
)?,
Member::Percent(value) => self.line(
level,
&format!(
"{display_name}: {}%",
crate::format::format_setting_number(value)
),
)?,
Member::Bool(value) => {
let rendered = self.setting_name(
"tokens",
if value { "On" } else { "Off" },
if value { "token.on" } else { "token.off" },
)?;
self.line(level, &format!("{display_name}: {rendered}"))?;
}
Member::YesNo(value) => {
let rendered = self.setting_name(
"tokens",
if value { "Yes" } else { "No" },
if value { "token.yes" } else { "token.no" },
)?;
self.line(level, &format!("{display_name}: {rendered}"))?;
}
Member::List { elements, kind } => {
self.line(level, &format!("{display_name} {{"))?;
for element in elements {
let english = kind
.resolve(element, name)
.map_err(|diagnostic| diagnostic.error)?;
let display = self.setting_name(
kind.section(),
english,
&format!("{}.{}.name", kind.label(), element.value),
)?;
self.line(level + 1, &display)?;
}
self.line(level, "}")?;
}
}
Ok(())
}
pub(crate) fn emit_opaque_group(
&mut self,
children: &[SettingsNode],
name: &str,
level: usize,
) -> Result<()> {
self.line(level, &format!("{name} {{"))?;
for child in children {
match child {
SettingsNode::Group { name, children, .. } => {
self.emit_opaque_group(children, name, level + 1)?;
}
_ => self.settings_member(child, level + 1, &[], None)?,
}
}
self.line(level, "}")?;
Ok(())
}
pub(crate) fn gameplay_setting_name(
&mut self,
hero: &str,
slot: &str,
id: &str,
) -> Result<String> {
let resolve = |locale: &Locale| {
crate::gameplay::data::builtin_ref()
.ok()
.and_then(|catalog| {
catalog
.query()
.ability_name(hero, slot, None, locale.as_str())
.ok()
.map(str::to_string)
})
};
if let Some(name) = resolve(&self.locale) {
return Ok(name);
}
if let Some(fallback) = &self.fallback {
if let Some(name) = resolve(fallback) {
if !self.fallback_ids.iter().any(|value| value == "settings") {
self.fallback_ids.push("settings".to_string());
}
return Ok(name);
}
}
Err(WorkshopError::MissingMapping {
kind: "setting",
id: id.to_string(),
locale: self.locale.clone(),
})
}
pub(crate) fn setting_name(
&mut self,
section: &str,
english: &str,
id: &str,
) -> Result<String> {
let en_us = Locale::new("en-US");
if self.locale == en_us {
return Ok(english.to_string());
}
if let Some(spelling) = table::localized_name(self.locale.as_str(), section, english) {
return Ok(spelling.to_string());
}
if let Some(fallback) = &self.fallback {
if *fallback == en_us {
if !self.fallback_ids.iter().any(|value| value == "settings") {
self.fallback_ids.push("settings".to_string());
}
return Ok(english.to_string());
}
}
Err(WorkshopError::MissingMapping {
kind: "setting",
id: id.to_string(),
locale: self.locale.clone(),
})
}
}
fn suggested(message: String, suggestion: Option<String>, span: Option<Span>) -> WorkshopError {
WorkshopError::malformed(
suggest::with_suggestion_text(message, suggestion.as_deref()),
span,
)
}