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 Program 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#[derive(Debug, Clone, PartialEq, Eq)]
36enum WIRActionLayoutError {
37    InvalidWIR(crate::wir::error::IrError),
38    Emission(crate::WorkshopError),
39}
40
41impl std::fmt::Display for WIRActionLayoutError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::InvalidWIR(error) => write!(f, "invalid WIR: {error}"),
45            Self::Emission(error) => write!(f, "action layout emission failed: {error}"),
46        }
47    }
48}
49
50impl std::error::Error for WIRActionLayoutError {}
51
52/// Query the native Workshop action width of a validated public action sequence.
53///
54/// The sequence is expanded using the same recursive action implementation as
55/// [`crate::emitter::emit`]. Every action in the sequence is treated as
56/// non-rule-final, which is the canonical stream contract needed for relative
57/// action offsets. The returned width counts native Workshop action lines,
58/// including structural headers and terminators.
59pub fn action_width(
60    program: &crate::Program,
61    catalog: &Catalog,
62    locale: &Locale,
63    actions: &[crate::Action],
64) -> std::result::Result<ActionLayout, ActionLayoutError> {
65    let mut input = crate::Program::new();
66    input.settings = program.settings.clone();
67    input.global_variables = program.global_variables.clone();
68    input.player_variables = program.player_variables.clone();
69    input.subroutines = program.subroutines.clone();
70    input
71        .rules
72        .push(crate::Rule::new("action layout", crate::Event::Global));
73    input.rules[0].actions = actions.to_vec();
74    let storage = input
75        .to_wir()
76        .map_err(|error| ActionLayoutError::InvalidProgram {
77            message: error.to_string(),
78        })?;
79    let rule = storage.rules.iter().next().ok_or_else(|| {
80        ActionLayoutError::Emission(crate::WorkshopError::Malformed {
81            message: "action layout program has no rule".to_string(),
82            span: None,
83        })
84    })?;
85    action_width_wir(&storage, catalog, locale, &rule.actions).map_err(|error| match error {
86        WIRActionLayoutError::InvalidWIR(error) => ActionLayoutError::InvalidProgram {
87            message: error.to_string(),
88        },
89        WIRActionLayoutError::Emission(error) => ActionLayoutError::Emission(error),
90    })
91}
92
93fn action_width_wir(
94    program: &wir::Program,
95    catalog: &Catalog,
96    locale: &Locale,
97    actions: &[wir::ActionId],
98) -> std::result::Result<ActionLayout, WIRActionLayoutError> {
99    program
100        .validate()
101        .map_err(WIRActionLayoutError::InvalidWIR)?;
102    let mut emitter = EmitContext {
103        program,
104        catalog,
105        locale: locale.clone(),
106        fallback: None,
107        fallback_ids: Vec::new(),
108        force_hero_constructors: false,
109        out: String::new(),
110        line_count: 0,
111    };
112    for action in actions {
113        emitter
114            .action(*action, 0, false)
115            .map_err(WIRActionLayoutError::Emission)?;
116    }
117    Ok(ActionLayout {
118        width: emitter.line_count,
119    })
120}