Skip to main content

workshop_rs/actions/
layout.rs

1//! Native Workshop layout queries for canonical actions.
2
3use crate::catalog::{Catalog, Locale};
4use crate::output::emitter::EmitContext;
5use crate::wir;
6
7/// The number of native Workshop actions emitted by a canonical WIR action
8/// sequence.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct ActionLayout {
11    /// The action count in the canonical native action stream.
12    pub width: usize,
13}
14
15/// Errors returned while querying public canonical native action layout.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ActionLayoutError {
18    /// The public program cannot be materialized as a valid Workshop program.
19    InvalidProgram { message: String },
20    /// Canonical emission could not expand the requested actions.
21    Emission(crate::WorkshopError),
22}
23
24impl std::fmt::Display for ActionLayoutError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::InvalidProgram { message } => write!(f, "invalid program: {message}"),
28            Self::Emission(error) => write!(f, "action layout emission failed: {error}"),
29        }
30    }
31}
32
33impl std::error::Error for ActionLayoutError {}
34
35#[doc(hidden)]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum WIRActionLayoutError {
38    InvalidWIR(crate::wir::error::IrError),
39    Emission(crate::WorkshopError),
40}
41
42impl std::fmt::Display for WIRActionLayoutError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::InvalidWIR(error) => write!(f, "invalid WIR: {error}"),
46            Self::Emission(error) => write!(f, "action layout emission failed: {error}"),
47        }
48    }
49}
50
51impl std::error::Error for WIRActionLayoutError {}
52
53/// Query the native Workshop action width of a validated public action sequence.
54///
55/// The sequence is expanded using the same recursive action implementation as
56/// [`crate::emitter::emit`]. Every action in the sequence is treated as
57/// non-rule-final, which is the canonical stream contract needed for relative
58/// action offsets. The returned width counts native Workshop action lines,
59/// including structural headers and terminators.
60pub fn action_width(
61    program: &crate::Program,
62    catalog: &Catalog,
63    locale: &Locale,
64    actions: &[crate::Action],
65) -> std::result::Result<ActionLayout, ActionLayoutError> {
66    let mut input = crate::Program::new();
67    input.settings = program.settings.clone();
68    input.global_variables = program.global_variables.clone();
69    input.player_variables = program.player_variables.clone();
70    input.subroutines = program.subroutines.clone();
71    input
72        .rules
73        .push(crate::Rule::new("action layout", crate::Event::Global));
74    input.rules[0].actions = actions.to_vec();
75    let storage = input
76        .to_wir()
77        .map_err(|error| ActionLayoutError::InvalidProgram {
78            message: error.to_string(),
79        })?;
80    let rule = storage.rules.iter().next().ok_or_else(|| {
81        ActionLayoutError::Emission(crate::WorkshopError::Malformed {
82            message: "action layout program has no rule".to_string(),
83            span: None,
84        })
85    })?;
86    action_width_wir(&storage, catalog, locale, &rule.actions).map_err(|error| match error {
87        WIRActionLayoutError::InvalidWIR(error) => ActionLayoutError::InvalidProgram {
88            message: error.to_string(),
89        },
90        WIRActionLayoutError::Emission(error) => ActionLayoutError::Emission(error),
91    })
92}
93
94#[doc(hidden)]
95pub fn action_width_wir(
96    program: &wir::Program,
97    catalog: &Catalog,
98    locale: &Locale,
99    actions: &[wir::ActionId],
100) -> std::result::Result<ActionLayout, WIRActionLayoutError> {
101    program
102        .validate()
103        .map_err(WIRActionLayoutError::InvalidWIR)?;
104    let mut emitter = EmitContext {
105        program,
106        catalog,
107        locale: locale.clone(),
108        fallback: None,
109        fallback_ids: Vec::new(),
110        force_hero_constructors: false,
111        out: String::new(),
112        line_count: 0,
113    };
114    for action in actions {
115        emitter
116            .action(*action, 0, false)
117            .map_err(WIRActionLayoutError::Emission)?;
118    }
119    Ok(ActionLayout {
120        width: emitter.line_count,
121    })
122}