bladeink/story/
progress.rs

1use crate::{
2    choice::Choice,
3    choice_point::ChoicePoint,
4    container::Container,
5    control_command::{CommandType, ControlCommand},
6    object::RTObject,
7    pointer::{self, Pointer},
8    push_pop::PushPopType,
9    story::{errors::ErrorType, OutputStateChange, Story},
10    story_error::StoryError,
11    value::Value,
12    void::Void,
13};
14use std::{self, rc::Rc};
15
16/// # Story Progress
17/// Methods to move the story forwards.
18impl Story {
19    /// `true` if the story is not waiting for user input from
20    /// [`choose_choice_index`](Story::choose_choice_index).
21    pub fn can_continue(&self) -> bool {
22        self.get_state().can_continue()
23    }
24
25    /// Tries to continue pulling text from the story.
26    pub fn cont(&mut self) -> Result<String, StoryError> {
27        self.continue_async(0.0)?;
28        self.get_current_text()
29    }
30
31    /// Continues the story until a choice or error is reached.
32    /// If a choice is reached, returns all text produced along the way.
33    pub fn continue_maximally(&mut self) -> Result<String, StoryError> {
34        self.if_async_we_cant("continue_maximally")?;
35
36        let mut sb = String::new();
37
38        while self.can_continue() {
39            sb.push_str(&self.cont()?);
40        }
41
42        Ok(sb)
43    }
44
45    /// Continues running the story code for the specified number of
46    /// milliseconds.
47    pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> {
48        if !self.has_validated_externals {
49            self.validate_external_bindings()?;
50        }
51
52        self.continue_internal(millisecs_limit_async)
53    }
54
55    pub(crate) fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> {
56        if self.async_continue_active {
57            return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a continue_async(). Make more continue_async() calls or a single cont() call beforehand.", activity_str)));
58        }
59
60        Ok(())
61    }
62
63    pub(crate) fn continue_internal(
64        &mut self,
65        millisecs_limit_async: f32,
66    ) -> Result<(), StoryError> {
67        let is_async_time_limited = millisecs_limit_async > 0.0;
68
69        self.recursive_continue_count += 1;
70
71        // Doing either:
72        // - full run through non-async (so not active and don't want to be)
73        // - Starting async run-through
74        if !self.async_continue_active {
75            self.async_continue_active = is_async_time_limited;
76            if !self.can_continue() {
77                return Err(StoryError::InvalidStoryState(
78                    "Can't continue - should check can_continue before calling Continue".to_owned(),
79                ));
80            }
81
82            self.get_state_mut().set_did_safe_exit(false);
83
84            self.get_state_mut().reset_output(None);
85
86            // It's possible for ink to call game to call ink to call game etc
87            // In this case, we only want to batch observe variable changes
88            // for the outermost call.
89            if self.recursive_continue_count == 1 {
90                self.state.variables_state.start_variable_observation();
91            }
92        } else if self.async_continue_active && !is_async_time_limited {
93            self.async_continue_active = false;
94        }
95
96        // Start timing (only when necessary)
97        let duration_stopwatch = match self.async_continue_active {
98            true => Some(web_time::Instant::now()),
99            false => None,
100        };
101
102        let mut output_stream_ends_in_newline = false;
103        self.saw_lookahead_unsafe_function_after_new_line = false;
104
105        loop {
106            match self.continue_single_step() {
107                Ok(r) => output_stream_ends_in_newline = r,
108                Err(e) => {
109                    self.add_error(e.get_message(), false);
110                    break;
111                }
112            }
113
114            if output_stream_ends_in_newline {
115                break;
116            }
117
118            // Run out of async time?
119            if self.async_continue_active
120                && duration_stopwatch.as_ref().unwrap().elapsed().as_millis() as f32
121                    > millisecs_limit_async
122            {
123                break;
124            }
125
126            if !self.can_continue() {
127                break;
128            }
129        }
130
131        let mut changed_variables_to_observe = None;
132
133        // 4 outcomes:
134        // - got newline (so finished this line of text)
135        // - can't continue (e.g. choices or ending)
136        // - ran out of time during evaluation
137        // - error
138        //
139        // Successfully finished evaluation in time (or in error)
140        if output_stream_ends_in_newline || !self.can_continue() {
141            // Need to rewind, due to evaluating further than we should?
142            if self.state_snapshot_at_last_new_line.is_some() {
143                self.restore_state_snapshot();
144            }
145
146            // Finished a section of content / reached a choice point?
147            if !self.can_continue() {
148                if self.state.get_callstack().borrow().can_pop_thread() {
149                    self.add_error("Thread available to pop, threads should always be flat by the end of evaluation?", false);
150                }
151
152                if self.state.get_generated_choices().is_empty()
153                    && !self.get_state().is_did_safe_exit()
154                    && self.temporary_evaluation_container.is_none()
155                {
156                    if self
157                        .state
158                        .get_callstack()
159                        .borrow()
160                        .can_pop_type(Some(PushPopType::Tunnel))
161                    {
162                        self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?", false);
163                    } else if self
164                        .state
165                        .get_callstack()
166                        .borrow()
167                        .can_pop_type(Some(PushPopType::Function))
168                    {
169                        self.add_error(
170                            "unexpectedly reached end of content. Do you need a '~ return'?",
171                            false,
172                        );
173                    } else if !self.get_state().get_callstack().borrow().can_pop() {
174                        self.add_error(
175                            "ran out of content. Do you need a '-> DONE' or '-> END'?",
176                            false,
177                        );
178                    } else {
179                        self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!", false);
180                    }
181                }
182            }
183            self.get_state_mut().set_did_safe_exit(false);
184            self.saw_lookahead_unsafe_function_after_new_line = false;
185
186            if self.recursive_continue_count == 1 {
187                changed_variables_to_observe =
188                    Some(self.state.variables_state.complete_variable_observation());
189            }
190
191            self.async_continue_active = false;
192        }
193
194        self.recursive_continue_count -= 1;
195
196        // Report any errors that occured during evaluation.
197        // This may either have been StoryExceptions that were thrown
198        // and caught during evaluation, or directly added with AddError.
199        if self.get_state().has_error() || self.get_state().has_warning() {
200            match &self.on_error {
201                Some(on_err) => {
202                    if self.get_state().has_error() {
203                        for err in self.get_state().get_current_errors() {
204                            on_err.borrow_mut().error(err, ErrorType::Error);
205                        }
206                    }
207
208                    if self.get_state().has_warning() {
209                        for err in self.get_state().get_current_warnings() {
210                            on_err.borrow_mut().error(err, ErrorType::Warning);
211                        }
212                    }
213
214                    self.reset_errors();
215                }
216                // Throw an exception since there's no error handler
217                None => {
218                    let mut sb = String::new();
219                    sb.push_str("Ink had ");
220
221                    if self.get_state().has_error() {
222                        sb.push_str(&self.get_state().get_current_errors().len().to_string());
223
224                        if self.get_state().get_current_errors().len() == 1 {
225                            sb.push_str(" error");
226                        } else {
227                            sb.push_str(" errors");
228                        }
229
230                        if self.get_state().has_warning() {
231                            sb.push_str(" and ");
232                        }
233                    }
234
235                    if self.get_state().has_warning() {
236                        sb.push_str(
237                            self.get_state()
238                                .get_current_warnings()
239                                .len()
240                                .to_string()
241                                .as_str(),
242                        );
243                        if self.get_state().get_current_errors().len() == 1 {
244                            sb.push_str(" warning");
245                        } else {
246                            sb.push_str(" warnings");
247                        }
248                    }
249
250                    sb.push_str(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: ");
251
252                    if self.get_state().has_error() {
253                        sb.push_str(self.get_state().get_current_errors()[0].as_str());
254                    } else {
255                        sb.push_str(
256                            self.get_state().get_current_warnings()[0]
257                                .to_string()
258                                .as_str(),
259                        );
260                    }
261
262                    // Send out variable observation events at the last second, since it might trigger new ink to be run
263                    if let Some(changed) = changed_variables_to_observe {
264                        for (variable_name, value) in changed {
265                            self.notify_variable_changed(&variable_name, &value);
266                        }
267                    }
268
269                    return Err(StoryError::InvalidStoryState(sb));
270                }
271            }
272        }
273
274        Ok(())
275    }
276
277    pub(crate) fn continue_single_step(&mut self) -> Result<bool, StoryError> {
278        // Run main step function (walks through content)
279        self.step()?;
280
281        // Run out of content and we have a default invisible choice that we can follow?
282        if !self.can_continue()
283            && !self
284                .get_state()
285                .get_callstack()
286                .borrow()
287                .element_is_evaluate_from_game()
288        {
289            self.try_follow_default_invisible_choice()?;
290        }
291
292        // Don't save/rewind during string evaluation, which is e.g. used for choices
293        if !self.get_state().in_string_evaluation() {
294            // We previously found a newline, but were we just double checking that
295            // it wouldn't immediately be removed by glue?
296            if let Some(state_snapshot_at_last_new_line) =
297                self.state_snapshot_at_last_new_line.as_mut()
298            {
299                // Has proper text or a tag been added? Then we know that the newline
300                // that was previously added is definitely the end of the line.
301                let change = Story::calculate_newline_output_state_change(
302                    &state_snapshot_at_last_new_line.get_current_text(),
303                    &self.state.get_current_text(),
304                    state_snapshot_at_last_new_line.get_current_tags().len() as i32,
305                    self.state.get_current_tags().len() as i32,
306                );
307
308                // The last time we saw a newline, it was definitely the end of the line, so we
309                // want to rewind to that point.
310                if change == OutputStateChange::ExtendedBeyondNewline
311                    || self.saw_lookahead_unsafe_function_after_new_line
312                {
313                    self.restore_state_snapshot();
314
315                    // Hit a newline for sure, we're done
316                    return Ok(true);
317                }
318                // Newline that previously existed is no longer valid - e.g.
319                // glue was encounted that caused it to be removed.
320                else if change == OutputStateChange::NewlineRemoved {
321                    self.state_snapshot_at_last_new_line = None;
322                    self.discard_snapshot();
323                }
324            }
325
326            // Current content ends in a newline - approaching end of our evaluation
327            if self.get_state().output_stream_ends_in_newline() {
328                // If we can continue evaluation for a bit:
329                // Create a snapshot in case we need to rewind.
330                // We're going to continue stepping in case we see glue or some
331                // non-text content such as choices.
332                if self.can_continue() {
333                    // Don't bother to record the state beyond the current newline.
334                    // e.g.:
335                    // Hello world\n // record state at the end of here
336                    // ~ complexCalculation() // don't actually need this unless it generates
337                    // text
338                    if self.state_snapshot_at_last_new_line.is_none() {
339                        self.state_snapshot();
340                    }
341                }
342                // Can't continue, so we're about to exit - make sure we
343                // don't have an old state hanging around.
344                else {
345                    self.discard_snapshot();
346                }
347            }
348        }
349
350        Ok(false)
351    }
352
353    pub(crate) fn step(&mut self) -> Result<(), StoryError> {
354        let mut should_add_to_stream = true;
355
356        // Get current content
357        let mut pointer = self.get_state().get_current_pointer().clone();
358
359        if pointer.is_null() {
360            return Ok(());
361        }
362
363        // Step directly to the first element of content in a container (if
364        // necessary)
365        let r = pointer.resolve();
366
367        let mut container_to_enter = match r {
368            Some(o) => match o.into_any().downcast::<Container>() {
369                Ok(c) => Some(c),
370                Err(_) => None,
371            },
372            None => None,
373        };
374
375        while let Some(cte) = container_to_enter.as_ref() {
376            // Mark container as being entered
377            self.visit_container(cte, true);
378
379            // No content? the most we can do is step past it
380            if cte.content.is_empty() {
381                break;
382            }
383
384            pointer = Pointer::start_of(cte.clone());
385
386            let r = pointer.resolve();
387
388            container_to_enter = match r {
389                Some(o) => match o.into_any().downcast::<Container>() {
390                    Ok(c) => Some(c),
391                    Err(_) => None,
392                },
393                None => None,
394            };
395        }
396
397        self.get_state_mut().set_current_pointer(pointer.clone());
398
399        // Is the current content Object:
400        // - Normal content
401        // - Or a logic/flow statement - if so, do it
402        // Stop flow if we hit a stack pop when we're unable to pop (e.g.
403        // return/done statement in knot
404        // that was diverted to rather than called as a function)
405        let mut current_content_obj = pointer.resolve();
406
407        let is_logic_or_flow_control = self.perform_logic_and_flow_control(&current_content_obj)?;
408
409        // Has flow been forced to end by flow control above?
410        if self.get_state().get_current_pointer().is_null() {
411            return Ok(());
412        }
413
414        if is_logic_or_flow_control {
415            should_add_to_stream = false;
416        }
417
418        // Choice with condition?
419        if let Some(cco) = &current_content_obj {
420            // If the container has no content, then it will be
421            // the "content" itself, but we skip over it.
422            if cco.as_any().is::<Container>() {
423                should_add_to_stream = false;
424            }
425
426            if let Ok(choice_point) = cco.clone().into_any().downcast::<ChoicePoint>() {
427                let choice = self.process_choice(&choice_point)?;
428                if let Some(choice) = choice {
429                    self.get_state_mut()
430                        .get_generated_choices_mut()
431                        .push(choice);
432                }
433
434                current_content_obj = None;
435                should_add_to_stream = false;
436            }
437        }
438
439        // Content to add to evaluation stack or the output stream
440        if should_add_to_stream {
441            // If we're pushing a variable pointer onto the evaluation stack,
442            // ensure that it's specific
443            // to our current (possibly temporary) context index. And make a
444            // copy of the pointer
445            // so that we're not editing the original runtime Object.
446            let var_pointer =
447                Value::get_variable_pointer_value(current_content_obj.as_ref().unwrap().as_ref());
448
449            if let Some(var_pointer) = var_pointer {
450                if var_pointer.context_index == -1 {
451                    // Create new Object so we're not overwriting the story's own
452                    // data
453                    let context_idx = self
454                        .get_state()
455                        .get_callstack()
456                        .borrow()
457                        .context_for_variable_named(&var_pointer.variable_name);
458                    current_content_obj = Some(Rc::new(Value::new_variable_pointer(
459                        &var_pointer.variable_name,
460                        context_idx as i32,
461                    )));
462                }
463            }
464
465            // Expression evaluation content
466            if self.get_state().get_in_expression_evaluation() {
467                self.get_state_mut()
468                    .push_evaluation_stack(current_content_obj.as_ref().unwrap().clone());
469            }
470            // Output stream content (i.e. not expression evaluation)
471            else {
472                self.get_state_mut()
473                    .push_to_output_stream(current_content_obj.as_ref().unwrap().clone());
474            }
475        }
476
477        // Increment the content pointer, following diverts if necessary
478        self.next_content()?;
479
480        // Starting a thread should be done after the increment to the content
481        // pointer,
482        // so that when returning from the thread, it returns to the content
483        // after this instruction.
484        if current_content_obj.is_some() {
485            if let Some(control_cmd) = current_content_obj
486                .as_ref()
487                .unwrap()
488                .as_any()
489                .downcast_ref::<ControlCommand>()
490            {
491                if control_cmd.command_type == CommandType::StartThread {
492                    self.get_state().get_callstack().borrow_mut().push_thread();
493                }
494            }
495        }
496
497        Ok(())
498    }
499
500    pub(crate) fn next_content(&mut self) -> Result<(), StoryError> {
501        // Setting previousContentObject is critical for
502        // VisitChangedContainersDueToDivert
503        let cp = self.get_state().get_current_pointer();
504        self.get_state_mut().set_previous_pointer(cp);
505
506        // Divert step?
507        if !self.get_state().diverted_pointer.is_null() {
508            let dp = self.get_state().diverted_pointer.clone();
509            self.get_state_mut().set_current_pointer(dp);
510            self.get_state_mut()
511                .set_diverted_pointer(pointer::NULL.clone());
512
513            // Internally uses state.previousContentObject and
514            // state.currentContentObject
515            self.visit_changed_containers_due_to_divert();
516
517            // Diverted location has valid content?
518            if !self.get_state().get_current_pointer().is_null() {
519                return Ok(());
520            }
521
522            // Otherwise, if diverted location doesn't have valid content,
523            // drop down and attempt to increment.
524            // This can happen if the diverted path is intentionally jumping
525            // to the end of a container - e.g. a Conditional that's
526            // re-joining
527        }
528
529        let successful_pointer_increment = self.increment_content_pointer();
530
531        // Ran out of content? Try to auto-exit from a function,
532        // or finish evaluating the content of a thread
533        if !successful_pointer_increment {
534            let mut did_pop = false;
535
536            let can_pop_type = self
537                .get_state()
538                .get_callstack()
539                .as_ref()
540                .borrow()
541                .can_pop_type(Some(PushPopType::Function));
542            if can_pop_type {
543                // Pop from the call stack
544                self.get_state_mut()
545                    .pop_callstack(Some(PushPopType::Function))?;
546
547                // This pop was due to dropping off the end of a function that
548                // didn't return anything,
549                // so in this case, we make sure that the evaluator has
550                // something to chomp on if it needs it
551                if self.get_state().get_in_expression_evaluation() {
552                    self.get_state_mut()
553                        .push_evaluation_stack(Rc::new(Void::new()));
554                }
555
556                did_pop = true;
557            } else if self
558                .get_state()
559                .get_callstack()
560                .as_ref()
561                .borrow()
562                .can_pop_thread()
563            {
564                self.get_state()
565                    .get_callstack()
566                    .as_ref()
567                    .borrow_mut()
568                    .pop_thread()?;
569
570                did_pop = true;
571            } else {
572                self.get_state_mut()
573                    .try_exit_function_evaluation_from_game();
574            }
575
576            // Step past the point where we last called out
577            if did_pop && !self.get_state().get_current_pointer().is_null() {
578                self.next_content()?;
579            }
580        }
581
582        Ok(())
583    }
584
585    pub(crate) fn increment_content_pointer(&self) -> bool {
586        let mut successful_increment = true;
587
588        let mut pointer = self
589            .get_state()
590            .get_callstack()
591            .as_ref()
592            .borrow()
593            .get_current_element()
594            .current_pointer
595            .clone();
596        pointer.index += 1;
597
598        let mut container = pointer.container.as_ref().unwrap().clone();
599
600        // Each time we step off the end, we fall out to the next container, all
601        // the
602        // while we're in indexed rather than named content
603        while pointer.index >= container.content.len() as i32 {
604            successful_increment = false;
605
606            let next_ancestor = container.get_object().get_parent();
607
608            if next_ancestor.is_none() {
609                break;
610            }
611
612            let rto: Rc<dyn RTObject> = container;
613            let index_in_ancestor = next_ancestor
614                .as_ref()
615                .unwrap()
616                .content
617                .iter()
618                .position(|s| Rc::ptr_eq(s, &rto));
619            if index_in_ancestor.is_none() {
620                break;
621            }
622
623            pointer = Pointer::new(next_ancestor, index_in_ancestor.unwrap() as i32);
624            container = pointer.container.as_ref().unwrap().clone();
625
626            // Increment to next content in outer container
627            pointer.index += 1;
628
629            successful_increment = true;
630        }
631
632        if !successful_increment {
633            pointer = pointer::NULL.clone();
634        }
635
636        self.get_state()
637            .get_callstack()
638            .as_ref()
639            .borrow_mut()
640            .get_current_element_mut()
641            .current_pointer = pointer;
642
643        successful_increment
644    }
645
646    pub(crate) fn calculate_newline_output_state_change(
647        prev_text: &str,
648        curr_text: &str,
649        prev_tag_count: i32,
650        curr_tag_count: i32,
651    ) -> OutputStateChange {
652        // Simple case: nothing's changed, and we still have a newline
653        // at the end of the current content
654        let newline_still_exists = curr_text.len() >= prev_text.len()
655            && !prev_text.is_empty()
656            && curr_text.as_bytes()[prev_text.len() - 1] == b'\n';
657        if prev_tag_count == curr_tag_count
658            && prev_text.len() == curr_text.len()
659            && newline_still_exists
660        {
661            return OutputStateChange::NoChange;
662        }
663
664        // Old newline has been removed, it wasn't the end of the line after all
665        if !newline_still_exists {
666            return OutputStateChange::NewlineRemoved;
667        }
668
669        // Tag added - definitely the start of a new line
670        if curr_tag_count > prev_tag_count {
671            return OutputStateChange::ExtendedBeyondNewline;
672        }
673
674        // There must be new content - check whether it's just whitespace
675        for c in curr_text.as_bytes().iter().skip(prev_text.len()) {
676            if *c != b' ' && *c != b'\t' {
677                return OutputStateChange::ExtendedBeyondNewline;
678            }
679        }
680
681        // There's new text but it's just spaces and tabs, so there's still the
682        // potential
683        // for glue to kill the newline.
684        OutputStateChange::NoChange
685    }
686
687    pub(crate) fn visit_container(&mut self, container: &Rc<Container>, at_start: bool) {
688        if !container.counting_at_start_only || at_start {
689            if container.visits_should_be_counted {
690                self.get_state_mut()
691                    .increment_visit_count_for_container(container);
692            }
693
694            if container.turn_index_should_be_counted {
695                self.get_state_mut()
696                    .record_turn_index_visit_to_container(container);
697            }
698        }
699    }
700
701    /// The vector of [`Choice`](crate::choice::Choice) objects available at
702    /// the current point in the `Story`. This vector will be
703    /// populated as the `Story` is stepped through with the
704    /// [`cont`](Story::cont) method.
705    /// Once [`can_continue`](Story::can_continue) becomes `false`, this
706    /// vector will be populated, and is usually (but not always) on the
707    /// final [`cont`](Story::cont) step.
708    pub fn get_current_choices(&self) -> Vec<Rc<Choice>> {
709        // Don't include invisible choices for external usage.
710        let mut choices = Vec::new();
711
712        if let Some(current_choices) = self.get_state().get_current_choices() {
713            for c in current_choices {
714                if !c.is_invisible_default {
715                    c.index.replace(choices.len());
716                    choices.push(c.clone());
717                }
718            }
719        }
720
721        choices
722    }
723
724    /// The string of output text available at the current point in
725    /// the `Story`. This string will be built as the `Story` is stepped
726    /// through with the [`cont`](Story::cont) method.
727    pub fn get_current_text(&mut self) -> Result<String, StoryError> {
728        self.if_async_we_cant("call currentText since it's a work in progress")?;
729        Ok(self.get_state_mut().get_current_text())
730    }
731}