use super::*;
use workshop_rs::source::{Position as WorkshopPosition, Span as WorkshopSpan};
pub(crate) fn workshop_settings(
hir: &crate::hir::Program,
) -> Result<(Option<workshop_rs::settings::Settings>, Vec<UnknownSetting>), IntegrationError> {
let settings_constants: HashMap<String, &Expr> = hir
.declarations
.iter()
.filter_map(|declaration| match declaration {
hir::Declaration::Constant { name, value, .. } => Some((name.clone(), value.as_ref())),
_ => None,
})
.collect();
let mut settings = merge_extensions(
hir.settings
.clone()
.map(|settings| expand_settings_constants(settings, &settings_constants)),
&hir.preprocessing.directives,
)?;
let unknown = settings
.as_mut()
.map(pass_through_unknown_members)
.unwrap_or_default();
Ok((settings, unknown))
}
pub(crate) struct UnknownSetting {
pub(crate) name: String,
pub(crate) value: Option<String>,
pub(crate) span: Option<WorkshopSpan>,
}
fn pass_through_unknown_members(
settings: &mut workshop_rs::settings::Settings,
) -> Vec<UnknownSetting> {
use workshop_rs::settings::{PathPart, SettingsNode};
let mut unknown = Vec::new();
for group in &mut settings.children {
let SettingsNode::Group { name, children, .. } = group else {
continue;
};
match name.as_str() {
"main" | "lobby" => {
pass_through_members(children, &[PathPart::Part(name)], &mut unknown);
}
"gamemodes" => {
for mode in children {
if let SettingsNode::Group { name, children, .. } = mode {
children.retain(|member| {
member.name() != "enabled"
|| matches!(member, SettingsNode::Bool { .. })
});
let path = [PathPart::Part("gamemodes"), PathPart::Part(name)];
pass_through_members(children, &path, &mut unknown);
}
}
}
"heroes" => {
for team in children {
let SettingsNode::Group { children, .. } = team else {
continue;
};
let mut general = Vec::new();
let mut rest = Vec::new();
for child in std::mem::take(children) {
match child {
SettingsNode::Group { name, children, .. } if name == "general" => {
general.extend(children);
}
child => rest.push(child),
}
}
let team_path = [PathPart::Part("heroes"), PathPart::Team];
pass_through_members(&mut general, &team_path, &mut unknown);
general.extend(rest);
*children = general;
for hero in children {
if let SettingsNode::Group { children, .. } = hero {
let path = [PathPart::Part("heroes"), PathPart::Team, PathPart::Hero];
pass_through_members(children, &path, &mut unknown);
}
}
}
}
_ => {}
}
}
unknown
}
fn pass_through_members(
members: &mut [workshop_rs::settings::SettingsNode],
path: &[workshop_rs::settings::PathPart<'_>],
unknown: &mut Vec<UnknownSetting>,
) {
use workshop_rs::settings::{PathPart, SettingValueDomain, SettingsNode};
for member in members {
if member.name() == "enabled" && matches!(path, [PathPart::Part("gamemodes"), _]) {
continue;
}
let name = member.name().to_string();
let span = member.span();
let mut full = path.to_vec();
full.push(PathPart::Part(&name));
match workshop_rs::settings::definition(&full) {
None => {
if matches!(member, SettingsNode::Raw { .. }) {
continue;
}
*member = written_form(member.clone());
unknown.push(UnknownSetting {
name,
value: None,
span,
});
}
Some(definition) => {
if !matches!(definition.domain(), SettingValueDomain::Enum { .. }) {
continue;
}
let value = match member {
SettingsNode::String { value, .. }
if !definition.enum_members().any(|member| member.id() == value) =>
{
value.clone()
}
SettingsNode::Number { .. } | SettingsNode::Bool { .. } => {
scalar_text(member).expect("scalar member")
}
_ => continue,
};
*member = SettingsNode::RawValue {
name: name.clone(),
value: value.clone(),
span,
};
unknown.push(UnknownSetting {
name,
value: Some(value),
span,
});
}
}
}
}
fn written_form(node: workshop_rs::settings::SettingsNode) -> workshop_rs::settings::SettingsNode {
use workshop_rs::settings::SettingsNode;
match node {
SettingsNode::List {
name,
elements,
span,
} => SettingsNode::Group {
name,
children: elements
.into_iter()
.map(|element| SettingsNode::Raw {
name: element.value,
value: String::new(),
span: element.span,
})
.collect(),
span,
},
SettingsNode::Group {
name,
children,
span,
} => SettingsNode::Group {
name,
children: children.into_iter().map(written_form).collect(),
span,
},
node => match scalar_text(&node) {
Some(value) => SettingsNode::RawValue {
name: node.name().to_string(),
value,
span: node.span(),
},
None => node,
},
}
}
fn scalar_text(node: &workshop_rs::settings::SettingsNode) -> Option<String> {
use workshop_rs::settings::SettingsNode;
match node {
SettingsNode::Number { value, .. } => {
Some(workshop_rs::format::format_setting_number(*value))
}
SettingsNode::Bool { value, .. } => Some(value.to_string()),
SettingsNode::String { value, .. } => Some(value.clone()),
_ => None,
}
}
pub(super) fn merge_extensions(
settings: Option<crate::hir::Settings>,
directives: &[crate::hir::DirectiveRecord],
) -> Result<Option<workshop_rs::settings::Settings>, IntegrationError> {
let mut extension_nodes: Vec<workshop_rs::settings::SettingsNode> = Vec::new();
for directive in directives
.iter()
.filter(|directive| directive.name == "extension")
{
let Some(name) = directive.value.as_deref() else {
return Err(IntegrationError::new(
"directive-invalid",
"`#!extension` expects one argument",
directive.span,
));
};
let span = directive.span;
if extension_nodes.iter().any(|node| node.name() == name) {
continue;
}
let path = [
workshop_rs::settings::PathPart::Part("extensions"),
workshop_rs::settings::PathPart::Part(name),
];
let Some(definition) = workshop_rs::settings::definition(&path) else {
return Err(IntegrationError::new(
"directive-invalid",
format!("unknown Workshop extension `{name}`"),
span,
));
};
if !matches!(
definition.domain(),
workshop_rs::settings::SettingValueDomain::PresenceOnly
) {
return Err(IntegrationError::new(
"directive-invalid",
format!("Workshop extension `{name}` is not a presence-only setting"),
span,
));
}
extension_nodes.push(workshop_rs::settings::SettingsNode::Flag {
name: name.to_string(),
span: span.map(convert_settings_span),
});
}
if extension_nodes.is_empty() {
return Ok(settings.map(convert_settings));
}
let mut settings = settings
.map(convert_settings)
.unwrap_or(workshop_rs::settings::Settings {
span: None,
children: Vec::new(),
});
if let Some(workshop_rs::settings::SettingsNode::Group { children, .. }) = settings
.children
.iter_mut()
.find(|node| node.name() == "extensions")
{
for extension in extension_nodes {
if !children.iter().any(|node| node.name() == extension.name()) {
children.push(extension);
}
}
} else {
settings
.children
.push(workshop_rs::settings::SettingsNode::Group {
name: "extensions".to_string(),
children: extension_nodes,
span: None,
});
}
Ok(Some(settings))
}
pub(super) fn expand_settings_constants(
settings: crate::hir::Settings,
constants: &HashMap<String, &Expr>,
) -> crate::hir::Settings {
crate::hir::Settings {
span: settings.span,
children: settings
.children
.into_iter()
.map(|node| expand_settings_node(node, constants))
.collect(),
}
}
fn expand_settings_node(
node: crate::hir::SettingsNode,
constants: &HashMap<String, &Expr>,
) -> crate::hir::SettingsNode {
use crate::hir::SettingsNode;
match node {
SettingsNode::Group {
name,
children,
span,
} => {
let children = children
.into_iter()
.map(|child| expand_settings_node(child, constants))
.collect::<Vec<_>>();
SettingsNode::Group {
name,
children,
span,
}
}
SettingsNode::Raw { name, value, span } => constants
.get(&value)
.and_then(|expr| settings_node_from_expr(name.clone(), expr, constants, span))
.unwrap_or(SettingsNode::Raw { name, value, span }),
node => node,
}
}
fn settings_node_from_expr(
name: String,
expr: &Expr,
constants: &HashMap<String, &Expr>,
span: Option<HirSpan>,
) -> Option<crate::hir::SettingsNode> {
use crate::hir::SettingsNode;
match expr {
Expr::Constant { name: value, .. } => constants
.get(value)
.and_then(|expr| settings_node_from_expr(name, expr, constants, span)),
Expr::Dict { entries, .. } => Some(SettingsNode::Group {
name,
children: entries
.iter()
.filter_map(|entry| {
let Expr::String {
value: child_name, ..
} = entry.key.as_ref()
else {
return None;
};
settings_node_from_expr(child_name.clone(), &entry.value, constants, entry.span)
})
.collect(),
span,
}),
Expr::Number { value, .. } => Some(SettingsNode::Number {
name,
value: *value,
span,
}),
Expr::Bool { value, .. } => Some(SettingsNode::Bool {
name,
value: *value,
span,
}),
Expr::String { value, .. } => Some(SettingsNode::String {
name,
value: value.clone(),
span,
}),
_ => None,
}
}
pub(super) fn convert_settings(settings: crate::hir::Settings) -> workshop_rs::settings::Settings {
workshop_rs::settings::Settings {
span: settings.span.map(convert_settings_span),
children: settings
.children
.into_iter()
.map(convert_settings_node)
.collect(),
}
}
fn convert_settings_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
use crate::hir::SettingsNode as SourceNode;
use workshop_rs::settings::{SettingsListElement, SettingsNode as TargetNode};
match node {
SourceNode::Group {
name,
children,
span,
} if name == "workshop" => TargetNode::Workshop {
children: children.into_iter().map(convert_workshop_node).collect(),
span: span.map(convert_settings_span),
},
SourceNode::Group {
name,
children,
span,
} => TargetNode::Group {
children: children
.into_iter()
.map(|child| escape_main_string(&name, child))
.map(convert_settings_node)
.collect(),
name,
span: span.map(convert_settings_span),
},
SourceNode::Number { name, value, span } => TargetNode::Number {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::Bool { name, value, span } => TargetNode::Bool {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::String { name, value, span } => TargetNode::String {
name: name.clone(),
value: if name == "mapRotation" && value == "afterGame" {
"afterAGame".to_string()
} else {
value
},
span: span.map(convert_settings_span),
},
SourceNode::Raw { name, value, span } => TargetNode::Raw {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::List {
name,
elements,
span,
} => TargetNode::List {
name,
elements: elements
.into_iter()
.map(|element| SettingsListElement {
value: element.value,
span: element.span.map(convert_settings_span),
})
.collect(),
span: span.map(convert_settings_span),
},
}
}
fn escape_main_string(group: &str, node: crate::hir::SettingsNode) -> crate::hir::SettingsNode {
use crate::hir::SettingsNode;
match node {
SettingsNode::String { name, value, span }
if group == "main" && matches!(name.as_str(), "modeName" | "description") =>
{
SettingsNode::String {
value: super::lowering::escape_bad_words(&value),
name,
span,
}
}
node => node,
}
}
fn convert_workshop_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
use crate::hir::SettingsNode as SourceNode;
use workshop_rs::settings::SettingsNode as TargetNode;
match node {
SourceNode::Group {
name,
children,
span,
} => TargetNode::Group {
name,
children: children.into_iter().map(convert_workshop_node).collect(),
span: span.map(convert_settings_span),
},
SourceNode::Number { name, value, span } => TargetNode::Raw {
name,
value: value.to_string(),
span: span.map(convert_settings_span),
},
SourceNode::Bool { name, value, span } => TargetNode::Raw {
name,
value: value.to_string(),
span: span.map(convert_settings_span),
},
SourceNode::String { name, value, span } | SourceNode::Raw { name, value, span } => {
TargetNode::Raw {
name,
value,
span: span.map(convert_settings_span),
}
}
SourceNode::List {
name,
elements,
span,
} => TargetNode::Raw {
name,
value: format!(
"[{}]",
elements
.into_iter()
.map(|element| element.value)
.collect::<Vec<_>>()
.join(", ")
),
span: span.map(convert_settings_span),
},
}
}
fn convert_settings_span(span: HirSpan) -> WorkshopSpan {
WorkshopSpan::new(
workshop_rs::source::FileId::from_index(span.file as usize),
WorkshopPosition::new(span.start.line, span.start.col),
WorkshopPosition::new(span.end.line, span.end.col),
)
}