opy-rs 0.1.61

Standalone OverPy-compatible .opy implementation with parsing, tooling, and bounded Workshop compilation.
Documentation
use super::*;
use workshop_rs::source::{Position as WorkshopPosition, Span as WorkshopSpan};

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<_>>();
            let children = if matches!(name.as_str(), "team1" | "team2" | "allTeams") {
                general_first(children)
            } else {
                children
            };
            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: general_first(
                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),
        },
    }
}

/// `main.modeName` and `main.description` are the settings strings the pinned
/// OverPy escapes for filtered words.
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),
    )
}

/// The hero-independent settings of a team come before its per-hero groups,
/// as the pinned OverPy writes them.
fn general_first(children: Vec<crate::hir::SettingsNode>) -> Vec<crate::hir::SettingsNode> {
    use crate::hir::SettingsNode;
    let mut general = Vec::new();
    let mut rest = Vec::new();
    for child in children {
        match child {
            SettingsNode::Group { name, children, .. } if name == "general" => {
                general.extend(children);
            }
            child => rest.push(child),
        }
    }
    general.extend(rest);
    general
}