use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use brink_format::{DefinitionId, PluralResolver, Value};
use crate::error::{RanOutOfContentCause, RuntimeError};
use crate::output::OutputBuffer;
use crate::program::Program;
use crate::rng::StoryRng;
use crate::state::ContextAccess;
use crate::vm;
use crate::world::{ResolvedPolicy, World};
use super::call_stack::{
CallFrame, CallFrameType, CallStack, ChoiceDisplay, ContainerPosition, ExecMode, Flow,
PendingTerminal, Thread,
};
use super::external::{ExternalFnHandler, ExternalResult, FunctionEval};
use super::types::{BlockId, Choice, Element, OutputLine, Stats, Step, StepOutcome, StoryStatus};
#[derive(Clone, Debug)]
pub struct FlowInstance {
pub(crate) flow: Flow,
pub(crate) status: StoryStatus,
pub(crate) stats: Stats,
pub(crate) eval: Option<EvalState>,
pub(crate) enforce_visibility: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct EvalState {
pub value_floor: usize,
pub choice_floor: usize,
}
#[derive(Debug, Clone)]
pub enum DriveOutcome {
Terminal(Vec<Step>),
AwaitingExternal(Vec<Step>),
}
impl FlowInstance {
pub fn new_at_root(program: &Program) -> (Self, World) {
Self::new_at(program, program.root_idx())
}
pub fn new_at(program: &Program, container_idx: u32) -> (Self, World) {
let globals = program.global_defaults();
let initial_frame = CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx,
offset: 0,
}],
frame_type: CallFrameType::Root,
external_fn_id: None,
function_output_start: None,
};
let initial_thread = Thread {
call_stack: CallStack::new(initial_frame),
};
let flow_instance = Self {
flow: Flow {
threads: vec![initial_thread],
value_stack: Vec::new(),
output: OutputBuffer::new(),
pending_choices: Vec::new(),
current_tags: Vec::new(),
in_tag: false,
skipping_choice: false,
did_safe_exit: false,
did_unsafe_yield: false,
ran_out_of_content_cause: RanOutOfContentCause::default(),
exec_mode: ExecMode::default(),
pure_callback: crate::story::PureCallbackState::default(),
next_block_id: 0,
pending_terminal: PendingTerminal::default(),
},
status: StoryStatus::Active,
stats: Stats::default(),
eval: None,
enforce_visibility: true,
};
let world = World::from_globals(globals, ResolvedPolicy::all_world());
(flow_instance, world)
}
pub fn set_visibility_enforcement(&mut self, enforce: bool) {
self.enforce_visibility = enforce;
}
#[must_use]
pub fn visibility_enforced(&self) -> bool {
self.enforce_visibility
}
pub fn set_exec_mode(&mut self, mode: ExecMode) {
self.flow.exec_mode = mode;
}
#[must_use]
pub fn exec_mode(&self) -> ExecMode {
self.flow.exec_mode
}
const STEP_LIMIT: u64 = 1_000_000;
pub fn step_single_line<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
) -> Result<Step, RuntimeError> {
match self.advance::<R>(program, line_tables, context, handler, resolver)? {
StepOutcome::Step(step) => Ok(step),
StepOutcome::AwaitingExternal => {
let id = self
.flow
.external_fn_id()
.ok_or(RuntimeError::CallStackUnderflow)?;
Err(RuntimeError::UnresolvedExternalCall(id))
}
}
}
pub const LINE_LIMIT: usize = 10_000;
pub fn drive_to_terminal<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
) -> Result<Vec<Step>, RuntimeError> {
let mut steps = Vec::new();
loop {
let step =
self.step_single_line::<R>(program, line_tables, context, handler, resolver)?;
let terminal = step.is_terminal();
steps.push(step);
if terminal {
return Ok(steps);
}
if steps.len() >= Self::LINE_LIMIT {
return Err(RuntimeError::LineLimitExceeded(Self::LINE_LIMIT));
}
}
}
pub fn drive<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
budget: &mut usize,
) -> Result<DriveOutcome, RuntimeError> {
let starting_budget = *budget;
let mut steps = Vec::new();
loop {
if *budget == 0 {
return Err(RuntimeError::LineLimitExceeded(starting_budget));
}
match self.advance::<R>(program, line_tables, context, handler, resolver)? {
StepOutcome::AwaitingExternal => return Ok(DriveOutcome::AwaitingExternal(steps)),
StepOutcome::Step(step) => {
let terminal = step.is_terminal();
*budget -= 1;
steps.push(step);
if terminal {
return Ok(DriveOutcome::Terminal(steps));
}
}
}
}
}
pub fn advance<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
) -> Result<StepOutcome, RuntimeError> {
self.advance_with_limit::<R>(
program,
line_tables,
context,
handler,
resolver,
Self::STEP_LIMIT,
)
}
#[expect(clippy::too_many_lines)]
pub(crate) fn advance_with_limit<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
step_limit: u64,
) -> Result<StepOutcome, RuntimeError> {
if let Some(pending) = self
.flow
.pending_terminal
.take_if_current(self.flow.next_block_id)
{
return Ok(StepOutcome::Step(pending));
}
if self.flow.output.has_completed_line()
&& let Some((text, tags, element)) =
self.flow
.output
.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
&self.flow, text, tags, element,
)));
}
if self.flow.output.has_unread() && self.status != StoryStatus::Active {
let (text, tags, element) =
flush_remaining(&mut self.flow, program, line_tables, resolver);
return Ok(StepOutcome::Step(yield_step(
self.status,
text,
tags,
element,
&mut self.flow,
program,
line_tables,
resolver,
)));
}
if self.status == StoryStatus::Ended {
return Err(RuntimeError::StoryEnded);
}
if self.status == StoryStatus::WaitingForChoice {
return Err(RuntimeError::NotWaitingForChoice);
}
if self.status == StoryStatus::Done {
if !self.flow.did_safe_exit {
return Err(RuntimeError::RanOutOfContent(
self.flow.ran_out_of_content_cause,
));
}
self.status = StoryStatus::Active;
self.flow.next_block_id += 1;
}
self.flow.did_safe_exit = false;
self.flow.did_unsafe_yield = false;
let Self {
flow,
status,
stats,
..
} = self;
let step_start = stats.steps;
loop {
stats.steps += 1;
if stats.steps - step_start > step_limit {
return Err(RuntimeError::StepLimitExceeded(step_limit));
}
let stepped = vm::step::<R>(flow, program, line_tables, context, stats, resolver)?;
stats.materializations += flow.drain_materializations();
match stepped {
vm::Stepped::Continue | vm::Stepped::ThreadCompleted => {
if flow.output.has_completed_line()
&& let Some((text, tags, element)) =
flow.output.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
flow, text, tags, element,
)));
}
}
vm::Stepped::ExternalCall => {
if !resolve_external_call(flow, program, handler)? {
return Ok(StepOutcome::AwaitingExternal);
}
if flow.output.has_completed_line()
&& let Some((text, tags, element)) =
flow.output.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
flow, text, tags, element,
)));
}
}
vm::Stepped::Done => {
context.increment_turn_index();
if !flow.pending_choices.is_empty() {
let all_invisible = flow
.pending_choices
.iter()
.all(|pc| pc.flags.is_invisible_default);
if all_invisible {
select_choice(flow, context, status, stats, 0)?;
if flow.output.has_completed_line()
&& let Some((text, tags, element)) =
flow.output.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
flow, text, tags, element,
)));
}
continue;
}
}
if flow.pending_choices.is_empty() {
*status = StoryStatus::Done;
} else {
*status = StoryStatus::WaitingForChoice;
stats.choices_presented += 1;
}
if flow.output.has_completed_line()
&& let Some((text, tags, element)) =
flow.output.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
flow, text, tags, element,
)));
}
let (text, tags, element) =
flush_remaining(flow, program, line_tables, resolver);
return Ok(StepOutcome::Step(yield_step(
*status,
text,
tags,
element,
flow,
program,
line_tables,
resolver,
)));
}
vm::Stepped::Ended => {
context.increment_turn_index();
*status = StoryStatus::Ended;
if flow.output.has_completed_line()
&& let Some((text, tags, element)) =
flow.output.take_first_line(program, line_tables, resolver)
{
return Ok(StepOutcome::Step(make_output_line(
flow, text, tags, element,
)));
}
let (text, tags, element) =
flush_remaining(flow, program, line_tables, resolver);
return Ok(StepOutcome::Step(yield_step(
*status,
text,
tags,
element,
flow,
program,
line_tables,
resolver,
)));
}
}
}
}
pub fn choose(
&mut self,
context: &mut (impl ContextAccess + ?Sized),
index: usize,
) -> Result<(), RuntimeError> {
if self.status != StoryStatus::WaitingForChoice {
return Err(RuntimeError::NotWaitingForChoice);
}
select_choice(
&mut self.flow,
context,
&mut self.status,
&mut self.stats,
index,
)
}
pub fn choose_path_string(
&mut self,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
path: &str,
) -> Result<(), RuntimeError> {
self.choose_path_string_with_args(program, context, path, &[])
}
pub fn choose_path_string_with_args(
&mut self,
program: &Program,
context: &mut (impl ContextAccess + ?Sized),
path: &str,
args: &[Value],
) -> Result<(), RuntimeError> {
if self.enforce_visibility && program.has_private_defs() && program.path_is_private(path) {
return Err(RuntimeError::PrivateAccess {
name: path.to_owned(),
});
}
if let Some(id) = self.flow.external_fn_id() {
let external = program
.external_fn(id)
.map_or_else(|| format!("{id}"), |e| program.name(e.name).to_owned());
return Err(RuntimeError::JumpWhileAwaitingExternal {
path: path.to_owned(),
external,
});
}
if self.eval.is_some() {
return Err(RuntimeError::AlreadyEvaluatingFunction);
}
let target_id = program
.find_path_target(path)
.ok_or_else(|| RuntimeError::UnknownPath(path.to_owned()))?;
let expected = program.path_param_count(path).unwrap_or(0);
if args.len() != expected as usize {
return Err(RuntimeError::ArgCountMismatch {
target: path.to_owned(),
expected,
got: args.len(),
});
}
let root_frame = CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: Vec::new(),
frame_type: CallFrameType::Root,
external_fn_id: None,
function_output_start: None,
};
self.flow.threads = vec![Thread {
call_stack: CallStack::new(root_frame),
}];
self.flow.pending_choices.clear();
self.flow.skipping_choice = false;
self.flow.in_tag = false;
self.flow.did_safe_exit = true;
self.flow.next_block_id += 1;
self.flow.value_stack.extend_from_slice(args);
vm::goto_target(&mut self.flow, program, context, target_id)?;
self.status = StoryStatus::Active;
Ok(())
}
#[must_use]
pub fn status(&self) -> StoryStatus {
self.status
}
#[must_use]
pub fn did_safe_exit(&self) -> bool {
self.flow.did_safe_exit
}
#[must_use]
pub fn stats(&self) -> &Stats {
&self.stats
}
#[must_use]
pub fn transcript(&self) -> &[crate::output::OutputPart] {
self.flow.output.transcript()
}
#[must_use]
pub fn transcript_len(&self) -> usize {
self.flow.output.transcript_len()
}
pub fn reset_cursor(&mut self) {
self.flow.output.reset_cursor();
}
#[must_use]
pub fn fragments(&self) -> &[crate::output::Fragment] {
self.flow.output.fragments()
}
#[must_use]
pub fn has_pending_external(&self) -> bool {
self.flow.external_fn_id().is_some()
}
#[must_use]
pub fn pending_external_fn_id(&self) -> Option<DefinitionId> {
self.flow.external_fn_id()
}
#[must_use]
pub fn pending_external_args(&self) -> &[Value] {
self.flow.external_args()
}
#[must_use]
pub fn pending_external_name<'p>(&self, program: &'p Program) -> Option<&'p str> {
let id = self.flow.external_fn_id()?;
let entry = program.external_fn(id)?;
Some(program.name(entry.name))
}
pub fn resolve_external(&mut self, value: Value) {
self.flow.resolve_external(value);
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (program, line tables, context, handler, resolver) plus the call target and args"
)]
pub fn begin_function_eval<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
container_idx: u32,
args: &[Value],
resolver: Option<&dyn PluralResolver>,
) -> Result<FunctionEval, RuntimeError> {
self.begin_function_eval_with_limit::<R>(
program,
line_tables,
context,
handler,
container_idx,
args,
resolver,
Self::STEP_LIMIT,
)
}
#[expect(
clippy::too_many_arguments,
reason = "the VM environment (program, line tables, context, handler, resolver) plus the call target, args, and step_limit"
)]
pub fn begin_function_eval_with_limit<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
container_idx: u32,
args: &[Value],
resolver: Option<&dyn PluralResolver>,
step_limit: u64,
) -> Result<FunctionEval, RuntimeError> {
if self.enforce_visibility
&& program.has_private_defs()
&& program.container_is_private(container_idx)
{
return Err(RuntimeError::PrivateAccess {
name: format!("{}", program.container(container_idx).id),
});
}
if self.eval.is_some() {
return Err(RuntimeError::AlreadyEvaluatingFunction);
}
let value_floor = self.flow.value_stack.len();
let choice_floor = self.flow.pending_choices.len();
self.flow.output.begin_capture();
let output_start = self.flow.output.target_len();
let boundary = CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx,
offset: 0,
}],
frame_type: CallFrameType::FunctionEvalFromGame,
external_fn_id: None,
function_output_start: Some(output_start),
};
self.flow.current_thread_mut().call_stack.push(boundary);
self.stats.frames_pushed += 1;
self.flow.value_stack.extend_from_slice(args);
self.eval = Some(EvalState {
value_floor,
choice_floor,
});
self.drive_function_eval::<R>(program, line_tables, context, handler, resolver, step_limit)
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors begin_function_eval: the VM environment plus the callee value and args"
)]
pub fn begin_function_value_eval<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
callee: &Value,
args: &[Value],
resolver: Option<&dyn PluralResolver>,
) -> Result<FunctionEval, RuntimeError> {
self.begin_function_value_eval_with_limit::<R>(
program,
line_tables,
context,
handler,
callee,
args,
resolver,
Self::STEP_LIMIT,
)
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors begin_function_eval_with_limit: the VM environment plus the callee value, args, and step_limit"
)]
pub fn begin_function_value_eval_with_limit<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
callee: &Value,
args: &[Value],
resolver: Option<&dyn PluralResolver>,
step_limit: u64,
) -> Result<FunctionEval, RuntimeError> {
if self.eval.is_some() {
return Err(RuntimeError::AlreadyEvaluatingFunction);
}
let (container_idx, _target, full_args) =
vm::prepare_fn_value_call(program, callee, args.to_vec())?;
if self.enforce_visibility
&& program.has_private_defs()
&& program.container_is_private(container_idx)
{
return Err(RuntimeError::PrivateAccess {
name: format!("{}", program.container(container_idx).id),
});
}
let value_floor = self.flow.value_stack.len();
let choice_floor = self.flow.pending_choices.len();
self.flow.output.begin_capture();
let output_start = self.flow.output.target_len();
let boundary = CallFrame {
return_address: None,
temps: Vec::new(),
container_stack: vec![ContainerPosition {
container_idx,
offset: 0,
}],
frame_type: CallFrameType::FunctionEvalFromGame,
external_fn_id: None,
function_output_start: Some(output_start),
};
self.flow.current_thread_mut().call_stack.push(boundary);
self.stats.frames_pushed += 1;
self.flow.value_stack.extend_from_slice(&full_args);
self.eval = Some(EvalState {
value_floor,
choice_floor,
});
self.drive_function_eval::<R>(program, line_tables, context, handler, resolver, step_limit)
}
pub fn resume_function_eval<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
) -> Result<FunctionEval, RuntimeError> {
self.resume_function_eval_with_limit::<R>(
program,
line_tables,
context,
handler,
resolver,
Self::STEP_LIMIT,
)
}
pub fn resume_function_eval_with_limit<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
step_limit: u64,
) -> Result<FunctionEval, RuntimeError> {
if self.eval.is_none() {
return Err(RuntimeError::NotEvaluatingFunction);
}
self.drive_function_eval::<R>(program, line_tables, context, handler, resolver, step_limit)
}
#[must_use]
pub fn is_evaluating_function(&self) -> bool {
self.eval.is_some()
}
fn drive_function_eval<R: StoryRng>(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
context: &mut (impl ContextAccess + ?Sized),
handler: &dyn ExternalFnHandler,
resolver: Option<&dyn PluralResolver>,
step_limit: u64,
) -> Result<FunctionEval, RuntimeError> {
let step_start = self.stats.steps;
loop {
self.stats.steps += 1;
if self.stats.steps - step_start > step_limit {
self.abort_eval(program, line_tables, resolver);
return Err(RuntimeError::StepLimitExceeded(step_limit));
}
let stepped = vm::step::<R>(
&mut self.flow,
program,
line_tables,
context,
&mut self.stats,
resolver,
)?;
self.stats.materializations += self.flow.drain_materializations();
match stepped {
vm::Stepped::Done | vm::Stepped::Ended => {
self.abort_eval(program, line_tables, resolver);
return Err(RuntimeError::FunctionYielded);
}
vm::Stepped::ExternalCall => {
if let Some(pending) =
self.resolve_eval_external(program, line_tables, resolver, handler)?
{
return Ok(pending);
}
}
vm::Stepped::Continue | vm::Stepped::ThreadCompleted => {}
}
if !self.flow.has_eval_boundary() {
let _captured = self.flow.output.end_capture(program, line_tables, resolver);
let floor = self.eval.take().map_or(0, |e| e.value_floor);
let mut ret: Option<Value> = None;
while self.flow.value_stack.len() > floor {
let v = self.flow.value_stack.pop();
if ret.is_none() {
ret = v; }
}
return Ok(FunctionEval::Returned(ret.unwrap_or(Value::Null)));
}
let choice_floor = self.eval.as_ref().map_or(0, |e| e.choice_floor);
if self.flow.pending_choices.len() > choice_floor {
self.abort_eval(program, line_tables, resolver);
return Err(RuntimeError::FunctionYielded);
}
}
}
fn resolve_eval_external(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
resolver: Option<&dyn PluralResolver>,
handler: &dyn ExternalFnHandler,
) -> Result<Option<FunctionEval>, RuntimeError> {
let fn_id = self
.flow
.external_fn_id()
.ok_or(RuntimeError::CallStackUnderflow)?;
let entry = program.external_fn(fn_id);
let fn_name = entry.map_or("?", |e| program.name(e.name));
match handler.call(fn_name, self.flow.external_args()) {
ExternalResult::Resolved(value) => {
self.flow.resolve_external(value);
Ok(None)
}
ExternalResult::Fallback => {
if let Some(fb_id) = entry.and_then(|e| e.fallback) {
let container_idx = program
.resolve_target(fb_id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
self.flow.invoke_fallback(container_idx);
Ok(None)
} else {
self.abort_eval(program, line_tables, resolver);
Err(RuntimeError::UnresolvedExternalCall(fn_id))
}
}
ExternalResult::Pending => Ok(Some(FunctionEval::AwaitingExternal)),
}
}
pub(crate) fn abort_eval(
&mut self,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
resolver: Option<&dyn PluralResolver>,
) {
if self.eval.take().is_some() {
let _ = self.flow.output.end_capture(program, line_tables, resolver);
}
}
}
#[expect(clippy::similar_names)]
fn select_choice(
flow: &mut Flow,
context: &mut (impl ContextAccess + ?Sized),
status: &mut StoryStatus,
stats: &mut Stats,
index: usize,
) -> Result<(), RuntimeError> {
let available = flow.pending_choices.len();
if index >= available {
return Err(RuntimeError::InvalidChoiceIndex { index, available });
}
let choice = flow.pending_choices.swap_remove(index);
let target_id = choice.target_id;
context.increment_visit(target_id);
context.set_turn_count(target_id, context.turn_index());
let current = flow.current_thread_mut();
*current = choice.thread_fork;
let frame = current
.call_stack
.last_mut()
.ok_or(RuntimeError::CallStackUnderflow)?;
frame.container_stack.clear();
frame.container_stack.push(ContainerPosition {
container_idx: choice.target_idx,
offset: choice.target_offset,
});
flow.pending_choices.clear();
*status = StoryStatus::Active;
stats.choices_selected += 1;
flow.next_block_id += 1;
Ok(())
}
fn resolve_external_call(
flow: &mut Flow,
program: &Program,
handler: &dyn ExternalFnHandler,
) -> Result<bool, RuntimeError> {
let fn_id = flow
.external_fn_id()
.ok_or(RuntimeError::CallStackUnderflow)?;
let entry = program.external_fn(fn_id);
let fn_name = entry.map_or("?", |e| program.name(e.name));
let result = handler.call(fn_name, flow.external_args());
match result {
ExternalResult::Resolved(value) => {
flow.resolve_external(value);
Ok(true)
}
ExternalResult::Fallback => {
let fallback_id = entry.and_then(|e| e.fallback);
if let Some(fb_id) = fallback_id {
let container_idx = program
.resolve_target(fb_id)
.map(|(idx, _)| idx)
.ok_or(RuntimeError::UnresolvedDefinition(fb_id))?;
flow.invoke_fallback(container_idx);
Ok(true)
} else {
Err(RuntimeError::UnresolvedExternalCall(fn_id))
}
}
ExternalResult::Pending => {
Ok(false)
}
}
}
fn flush_remaining(
flow: &mut Flow,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
resolver: Option<&dyn brink_format::PluralResolver>,
) -> (String, Vec<String>, BTreeMap<String, String>) {
let lines = flow.output.flush_lines(program, line_tables, resolver);
let mut text = String::new();
let mut tags = Vec::new();
let mut element = BTreeMap::new();
for (i, (line_text, line_tags, line_element)) in lines.iter().enumerate() {
if i > 0 {
text.push('\n');
}
text.push_str(line_text);
tags.extend_from_slice(line_tags);
element.extend(line_element.iter().map(|(k, v)| (k.clone(), v.clone())));
}
(text, tags, element)
}
fn make_output_line(
flow: &Flow,
text: String,
tags: Vec<String>,
data: BTreeMap<String, String>,
) -> Step {
let element = if data.is_empty() {
Element::narrative()
} else {
Element {
kind: Element::NARRATIVE.to_string(),
data,
}
};
Step::Line(OutputLine {
text,
tags,
block_id: BlockId(flow.next_block_id),
element,
})
}
fn collect_choices(
flow: &Flow,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
resolver: Option<&dyn brink_format::PluralResolver>,
) -> Vec<Choice> {
flow.pending_choices
.iter()
.enumerate()
.filter(|(_, pc)| !pc.flags.is_invisible_default)
.map(|(i, pc)| {
let display_text = match &pc.display {
ChoiceDisplay::Text(s) => s.clone(),
ChoiceDisplay::Fragment(idx) => {
flow.output
.resolve_fragment(*idx, program, line_tables, resolver)
}
};
let display_text = display_text
.trim_matches(|c: char| c == ' ' || c == '\t')
.to_string();
Choice {
text: display_text,
index: i,
tags: pc.tags.clone(),
}
})
.collect()
}
#[expect(
clippy::too_many_arguments,
reason = "issue #2108's `element` param pushed this past 7; each param is \
a distinct piece of the terminal/line it builds, not a natural group"
)]
fn yield_step(
status: StoryStatus,
text: String,
tags: Vec<String>,
element: BTreeMap<String, String>,
flow: &mut Flow,
program: &Program,
line_tables: &[Vec<brink_format::LineEntry>],
resolver: Option<&dyn brink_format::PluralResolver>,
) -> Step {
let terminal = match status {
StoryStatus::WaitingForChoice => {
Step::Choices(collect_choices(flow, program, line_tables, resolver))
}
StoryStatus::Ended => Step::End,
StoryStatus::Done => Step::Done,
StoryStatus::Active => return make_output_line(flow, text, tags, element),
};
if text.is_empty() && tags.is_empty() {
terminal
} else {
flow.pending_terminal.stash(flow.next_block_id, terminal);
make_output_line(flow, text, tags, element)
}
}