Skip to main content

multilinear_story/
lib.rs

1#![deny(missing_docs)]
2
3//! Game-facing story runtime over a [`multilinear`] net.
4//!
5//! Loads a multilinear net (`.mld`: aspect defaults before the first header, then events)
6//! together with a presentation *wiring* file, then routes the currently callable events to
7//! their owners and reads aspect state. This is the shared substrate behind the "callable-events-as-options"
8//! pattern: an interactable's options are the callable events wired to it, applying one is a
9//! transition, and the net re-derives availability on its own.
10//!
11//! ```text
12//! # events
13//! ## bibo talk
14//! owner bibo
15//! dialog bibo_talk
16//!
17//! # idle
18//! ## bibo
19//! dialog bibo_idle
20//! ```
21//!
22//! The wiring is header-based (via `header-parsing`): event names are header segments, so they
23//! may contain whitespace. Each wired event must exist in the net or loading fails.
24
25use std::{collections::HashMap, convert::Infallible, io::Error as IoError, path::Path};
26
27use event_simulation::{Simulation, SimulationInfo, SimulationState};
28use header_parsing::parse_header;
29use multilinear::{MultilinearInfo, MultilinearSimulation};
30use multilinear_parser::{NamedMultilinearInfo, parse_multilinear, parse_multilinear_extended};
31use thiserror::Error;
32
33pub use multilinear::{Aspect, Event};
34
35/// The presentation wired to an event: which interactable surfaces it, and which dialog to play.
36pub struct Presentation {
37    /// The owner that surfaces this event (an NPC id, a marker, a place).
38    pub owner: Box<str>,
39    /// The dialog key to play when this event is chosen.
40    pub dialog: Box<str>,
41}
42
43/// An error while loading a story.
44#[derive(Debug, Error)]
45pub enum LoadError {
46    /// The net (`.mld`/`.mla`) failed to parse.
47    #[error("failed to parse story net: {0}")]
48    Net(String),
49    /// The wiring file could not be read.
50    #[error("failed to read wiring file: {0}")]
51    Wiring(#[from] IoError),
52    /// The wiring references an event that does not exist in the net.
53    #[error("wiring references unknown event: {0}")]
54    UnknownEvent(Box<str>),
55}
56
57/// A loaded story: the net simulation plus its name maps and presentation wiring.
58pub struct Story {
59    simulation: MultilinearSimulation,
60    events: HashMap<Box<str>, Event>,
61    aspects: HashMap<Box<str>, (Aspect, Vec<Box<str>>)>,
62    presentation: HashMap<Event, Presentation>,
63    idle: HashMap<Box<str>, Box<str>>,
64}
65
66impl Story {
67    /// Loads a story from a net (`.mld`, aspect defaults before the first header) and a wiring file.
68    ///
69    /// Fails if the net does not parse, the wiring cannot be read, or the wiring names an event
70    /// the net does not define. Events in the net without wiring are reported as warnings (they
71    /// are reachable but never surfaced) and left out of the presentation map.
72    pub fn load(net: &Path, wiring: &Path) -> Result<Self, LoadError> {
73        let named = parse_multilinear_extended(net, None)
74            .map_err(|error| LoadError::Net(error.to_string()))?;
75        let wiring = parse_wiring(wiring)?;
76        Self::from_parts(named, wiring)
77    }
78
79    /// Loads a story like [`load`](Self::load), but restores a previously saved
80    /// aspect state (from [`state`](Self::state)) instead of starting fresh.
81    ///
82    /// The state is the raw aspect value indices; a length or version mismatch
83    /// with the current net yields an inconsistent story rather than an error,
84    /// so only restore state saved against the same net.
85    pub fn load_with_state(
86        net: &Path,
87        wiring: &Path,
88        state: Vec<usize>,
89    ) -> Result<Self, LoadError> {
90        let named = parse_multilinear_extended(net, None)
91            .map_err(|error| LoadError::Net(error.to_string()))?;
92        let wiring = parse_wiring(wiring)?;
93        Self::from_parts_with_state(named, wiring, Some(state))
94    }
95
96    /// Returns the current aspect state as raw value indices, for saving.
97    ///
98    /// Restore it with [`load_with_state`](Self::load_with_state) against the
99    /// same net.
100    pub fn state(&self) -> Vec<usize> {
101        self.simulation.data().to_vec()
102    }
103
104    /// Loads a story from in-memory net and wiring text.
105    ///
106    /// The string equivalent of [`load`](Self::load), for embedded or fetched
107    /// content (e.g. on `wasm32` where there is no filesystem). `net` is the
108    /// `.mld` net source; `wiring` is the wiring file source.
109    pub fn load_from_str(net: &str, wiring: &str) -> Result<Self, LoadError> {
110        let named =
111            parse_multilinear(net.as_bytes()).map_err(|error| LoadError::Net(error.to_string()))?;
112        Self::from_parts(named, parse_wiring_str(wiring))
113    }
114
115    fn from_parts(named: NamedMultilinearInfo, wiring: Wiring) -> Result<Self, LoadError> {
116        Self::from_parts_with_state(named, wiring, None)
117    }
118
119    fn from_parts_with_state(
120        named: NamedMultilinearInfo,
121        wiring: Wiring,
122        state: Option<Vec<usize>>,
123    ) -> Result<Self, LoadError> {
124        let events: HashMap<Box<str>, Event> = (&named.events)
125            .into_iter()
126            .map(|(event, namespace)| (join_namespace(namespace), event))
127            .collect();
128
129        let aspects: HashMap<Box<str>, (Aspect, Vec<Box<str>>)> = (&named.aspects)
130            .into_iter()
131            .map(|(aspect, (name, values))| (name.clone(), (aspect, values.clone())))
132            .collect();
133
134        let Wiring {
135            events: wired,
136            idle,
137        } = wiring;
138
139        let mut presentation = HashMap::new();
140        for (name, entry) in wired {
141            let Some(&event) = events.get(&name) else {
142                return Err(LoadError::UnknownEvent(name));
143            };
144            presentation.insert(event, entry);
145        }
146
147        for (name, event) in &events {
148            if !presentation.contains_key(event) {
149                eprintln!("warning: net event has no wiring (never surfaced): {name}");
150            }
151        }
152
153        let simulation = match state {
154            Some(data) => MultilinearSimulation::from_data(named.info, data)
155                .unwrap_or_else(|error: Infallible| match error {}),
156            None => MultilinearSimulation::new(named.info),
157        };
158
159        Ok(Self {
160            simulation,
161            events,
162            aspects,
163            presentation,
164            idle,
165        })
166    }
167
168    /// An empty story, for use as a fallback when loading fails.
169    pub fn empty() -> Self {
170        Self {
171            simulation: MultilinearSimulation::new(MultilinearInfo::default()),
172            events: HashMap::new(),
173            aspects: HashMap::new(),
174            presentation: HashMap::new(),
175            idle: HashMap::new(),
176        }
177    }
178
179    /// Returns the event of the given name, if it exists.
180    pub fn event(&self, name: &str) -> Option<Event> {
181        self.events.get(name).copied()
182    }
183
184    /// Iterates over the aspects and their names.
185    pub fn aspects(&self) -> impl Iterator<Item = (&str, Aspect)> {
186        self.aspects
187            .iter()
188            .map(|(name, (aspect, _))| (name.as_ref(), *aspect))
189    }
190
191    /// Returns the aspect of the given name, if it exists.
192    pub fn aspect(&self, name: &str) -> Option<Aspect> {
193        self.aspects.get(name).map(|(aspect, _)| *aspect)
194    }
195
196    /// Returns the current value index of an aspect.
197    pub fn value(&self, aspect: Aspect) -> usize {
198        self.simulation.data()[aspect.0]
199    }
200
201    /// Returns the current value name of an aspect, looked up by aspect name.
202    pub fn value_name(&self, name: &str) -> Option<&str> {
203        let (aspect, values) = self.aspects.get(name)?;
204        values
205            .get(self.simulation.data()[aspect.0])
206            .map(AsRef::as_ref)
207    }
208
209    /// Returns the value names an aspect can take, in index order.
210    pub fn values(&self, name: &str) -> Option<impl Iterator<Item = &str>> {
211        let (_, values) = self.aspects.get(name)?;
212        Some(values.iter().map(AsRef::as_ref))
213    }
214
215    /// Sets an aspect to one of its values by name, bypassing the events that
216    /// would normally reach it.
217    ///
218    /// Returns whether the aspect and the value exist. Intended for debugging,
219    /// testing and starting a session from a chosen situation; regular play
220    /// should go through [`play`](Self::play).
221    pub fn set_value(&mut self, name: &str, value: &str) -> bool {
222        let Some((aspect, values)) = self.aspects.get(name) else {
223            return false;
224        };
225        let Some(index) = values
226            .iter()
227            .position(|candidate| candidate.as_ref() == value)
228        else {
229            return false;
230        };
231
232        let mut data = self.simulation.data().to_vec();
233        data[aspect.0] = index;
234        let state = self
235            .simulation
236            .load_state(data)
237            .unwrap_or_else(|error: Infallible| match error {});
238        self.simulation.state = state;
239        true
240    }
241
242    /// Whether an event is currently callable.
243    pub fn callable(&self, event: Event) -> bool {
244        self.simulation.callable(event)
245    }
246
247    /// All currently callable events, sorted.
248    pub fn callable_events(&self) -> Vec<Event> {
249        let mut events: Vec<Event> = self.simulation.callables().collect();
250        events.sort();
251        events
252    }
253
254    /// The callable events wired to the given owner, sorted — the owner's current options.
255    pub fn options(&self, owner: &str) -> Vec<Event> {
256        let mut events: Vec<Event> = self
257            .simulation
258            .callables()
259            .filter(|event| {
260                self.presentation
261                    .get(event)
262                    .is_some_and(|presentation| presentation.owner.as_ref() == owner)
263            })
264            .collect();
265        events.sort();
266        events
267    }
268
269    /// The presentation wired to an event, if any.
270    pub fn presentation(&self, event: Event) -> Option<&Presentation> {
271        self.presentation.get(&event)
272    }
273
274    /// The dialog key wired to an event, if any.
275    pub fn dialog(&self, event: Event) -> Option<&str> {
276        self.presentation.get(&event).map(|p| p.dialog.as_ref())
277    }
278
279    /// The owner wired to an event, if any.
280    pub fn owner(&self, event: Event) -> Option<&str> {
281        self.presentation.get(&event).map(|p| p.owner.as_ref())
282    }
283
284    /// The idle dialog for an owner with no callable options, if wired.
285    pub fn idle(&self, owner: &str) -> Option<&str> {
286        self.idle.get(owner).map(AsRef::as_ref)
287    }
288
289    /// Applies an event, advancing the net. Returns whether it was callable.
290    pub fn play(&mut self, event: Event) -> bool {
291        self.simulation.try_call(event)
292    }
293
294    /// Reverts an event, undoing its transition. Returns whether it was revertable.
295    pub fn revert(&mut self, event: Event) -> bool {
296        self.simulation.try_revert(event)
297    }
298}
299
300fn join_namespace(namespace: &[Box<str>]) -> Box<str> {
301    namespace
302        .iter()
303        .map(AsRef::as_ref)
304        .collect::<Vec<&str>>()
305        .join(" ")
306        .into_boxed_str()
307}
308
309struct Wiring {
310    events: HashMap<Box<str>, Presentation>,
311    idle: HashMap<Box<str>, Box<str>>,
312}
313
314fn parse_wiring(path: &Path) -> Result<Wiring, IoError> {
315    let content = std::fs::read_to_string(path)?;
316    Ok(parse_wiring_str(&content))
317}
318
319fn parse_wiring_str(content: &str) -> Wiring {
320    let mut events: HashMap<Box<str>, Presentation> = HashMap::new();
321    let mut idle: HashMap<Box<str>, Box<str>> = HashMap::new();
322    let mut header_path: Vec<Box<str>> = Vec::new();
323
324    for line in content.lines() {
325        if let Some(result) = parse_header(&mut header_path, line) {
326            match result {
327                Ok(changes) => {
328                    changes.apply();
329                }
330                Err(_) => eprintln!("invalid header in wiring: {line}"),
331            }
332            continue;
333        }
334
335        let trimmed = line.trim();
336        if trimmed.is_empty() {
337            continue;
338        }
339
340        let mut tokens = trimmed.splitn(2, char::is_whitespace);
341        let Some(key) = tokens.next() else { continue };
342        let value = tokens.next().unwrap_or("").trim();
343
344        match header_path.as_slice() {
345            [section, event] if section.as_ref() == "events" => {
346                let entry = events.entry(event.clone()).or_insert_with(|| Presentation {
347                    owner: Box::from(""),
348                    dialog: Box::from(""),
349                });
350                match key {
351                    "owner" => entry.owner = Box::from(value),
352                    "dialog" => entry.dialog = Box::from(value),
353                    _ => {}
354                }
355            }
356            [section, owner] if section.as_ref() == "idle" && key == "dialog" => {
357                idle.insert(owner.clone(), Box::from(value));
358            }
359            _ => {}
360        }
361    }
362
363    Wiring { events, idle }
364}