use thiserror::Error;
use crate::dialogue::{DialogueNode, NodeName, Step, StepId, StepKind};
#[derive(Error, Debug)]
pub(crate) enum BuildError {
#[error("Dangling target found from {from:?} to {to:?}")]
DanglingTarget { from: StepId, to: StepId },
#[error("Entry is dangling")]
DanglingEntry,
#[error("Empty node")]
EmptyNode,
#[error("Self referencing for {0:?}")]
SelfReferencing(StepId),
#[error("An expr is in error")]
ErrorInExpr,
#[error("Step {0:?} was reserved but never filled")]
UnfilledStep(StepId),
}
#[derive(Default)]
pub(crate) struct DialogueNodeBuilder {
steps: Vec<Option<StepKind>>,
}
impl DialogueNodeBuilder {
pub fn push(&mut self, kind: StepKind) -> StepId {
self.steps.push(Some(kind));
StepId((self.steps.len() - 1) as u32)
}
pub fn reserve(&mut self) -> StepId {
self.steps.push(None);
StepId((self.steps.len() - 1) as u32)
}
pub fn fill(&mut self, id: StepId, kind: StepKind) {
let slot = self
.steps
.get_mut(id.id() as usize)
.expect("filling an id that was never handed out");
debug_assert!(slot.is_none(), "filling {id:?} twice");
*slot = Some(kind);
}
pub fn build(self, name: NodeName, entry: StepId) -> Result<DialogueNode, BuildError> {
if self.steps.is_empty() {
return Err(BuildError::EmptyNode);
}
let max_id = (self.steps.len() - 1) as u32;
if entry.id() > max_id {
return Err(BuildError::DanglingEntry);
}
let steps = self
.steps
.into_iter()
.enumerate()
.map(|(id, kind)| {
let id = id as u32;
let kind = kind.ok_or(BuildError::UnfilledStep(StepId(id)))?;
match &kind {
StepKind::Say { next, .. } => {
if next.id() == id {
return Err(BuildError::SelfReferencing(StepId(id)));
}
}
StepKind::Command { next, .. } => {
if next.id() == id {
return Err(BuildError::SelfReferencing(StepId(id)));
}
}
StepKind::Choice { choices }
if choices.iter().find(|c| c.target.id() == id).is_some() =>
{
return Err(BuildError::SelfReferencing(StepId(id)));
}
_ => (),
}
match has_dangling_target(&kind, max_id) {
Some(t) => Err(BuildError::DanglingTarget {
from: StepId(id),
to: t,
}),
None => Ok(Step { kind }),
}
})
.collect::<Result<Vec<_>, BuildError>>()?;
Ok(DialogueNode { name, entry, steps })
}
}
fn has_dangling_target(kind: &StepKind, max_id: u32) -> Option<StepId> {
let targets = match kind {
StepKind::Say { next, .. } => vec![next],
StepKind::Command { next, .. } => vec![next],
StepKind::Set { next, .. } => vec![next],
StepKind::Branch {
then, otherwise, ..
} => vec![then, otherwise],
StepKind::Choice { choices } => choices.iter().map(|c| &c.target).collect(),
StepKind::Jump(_) | StepKind::End => vec![],
};
targets.into_iter().find(|t| t.id() > max_id).copied()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialogue::{ChoiceDef, TextLine, TextPart};
fn line(next: StepId) -> StepKind {
StepKind::Say {
line: TextLine {
speaker: None,
text: vec![TextPart::Text("...".into())],
},
next,
}
}
fn choice(targets: &[StepId]) -> StepKind {
StepKind::Choice {
choices: targets
.iter()
.map(|t| ChoiceDef {
text: vec![TextPart::Text("...".into())],
target: *t,
})
.collect(),
}
}
#[test]
fn push_returns_sequential_ids() {
let mut b = DialogueNodeBuilder::default();
assert_eq!(b.push(StepKind::End).id(), 0);
assert_eq!(b.push(StepKind::End).id(), 1);
assert_eq!(b.push(StepKind::End).id(), 2);
}
#[test]
fn build_keeps_steps_in_push_order() {
let mut b = DialogueNodeBuilder::default();
let end = b.push(StepKind::End);
let l0 = b.push(line(end));
let node = b.build(NodeName::new("start"), l0).unwrap();
assert_eq!(node.name, NodeName::new("start"));
assert_eq!(node.entry, l0);
assert_eq!(node.steps.len(), 2);
assert!(matches!(node.get_step(&end).kind, StepKind::End));
assert!(matches!(node.get_step(&l0).kind, StepKind::Say { .. }));
}
#[test]
fn error_on_self_referencing_line() {
let mut b = DialogueNodeBuilder::default();
let l0 = b.push(line(StepId(0)));
assert!(matches!(
b.build(NodeName::new("start"), l0),
Err(BuildError::SelfReferencing(_))
));
}
#[test]
fn error_on_dangling_line_target() {
let mut b = DialogueNodeBuilder::default();
let l0 = b.push(line(StepId(42)));
let Err(BuildError::DanglingTarget { from, to }) = b.build(NodeName::new("start"), l0)
else {
panic!("expected a DanglingTarget error");
};
assert_eq!(from, l0);
assert_eq!(to, StepId(42));
}
#[test]
fn error_on_dangling_choice_target() {
let mut b = DialogueNodeBuilder::default();
let end = b.push(StepKind::End);
let c = b.push(choice(&[end, StepId(7)]));
let Err(BuildError::DanglingTarget { from, to }) = b.build(NodeName::new("start"), c)
else {
panic!("expected a DanglingTarget error");
};
assert_eq!(from, c);
assert_eq!(to, StepId(7));
}
}