Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/bevy_plugin/assets/options.yarn
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ Ancient Reptilian Brain: Never ever.
title: Hub0
position: -43,-216
---
-> You: Never ever ever? <<if $never == false>>
-> You: Never ever ever? <<if $never == false>> #line:x1
Ancient Reptilian Brain: Never ever ever ever, baby!
<<set $never to true>>
<<jump Hub0>>
-> You: (Simply keep on non-existing.)
-> You: (Simply keep on non-existing.) #line:x2
Ancient Reptilian Brain: An inordinate amount of time passes. It is utterly void of struggle. No ex-wives are contained within it.

<<set $great to false>>
Expand Down
19 changes: 19 additions & 0 deletions crates/bevy_plugin/src/dialogue_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ impl DialogueRunner {
Ok(self)
}

/// If the dialogue is currently waiting for the user to select an option, this method will select the option with the given id.
/// Implies [`DialogueRunner::continue_in_next_update`].
pub fn select_option_by_line_id(&mut self, line: LineId) -> Result<&mut Self> {
if !self.is_running {
bail!(
"Can't select option of line id {line}: the dialogue is currently not running. Please call `DialogueRunner::continue_in_next_update()` only after receiving a `PresentOptionsEvent`."
)
}

let option = self
.inner_mut()
.0
.set_selected_option_by_line_id(line)
.map_err(Error::from)?;
self.last_selected_option.replace(option);
self.continue_in_next_update();
Ok(self)
}

/// Returns whether the dialogue runner is currently running. Returns `false` if:
/// - The dialogue has not yet been started via [`DialogueRunner::start_node`]
/// - The dialogue has been stopped via [`DialogueRunner::stop`]
Expand Down
41 changes: 41 additions & 0 deletions crates/bevy_plugin/tests/test_dialogue_runner_delivers_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ fn errs_on_unexpected_selection_value() -> Result<()> {
Ok(())
}

#[test]
fn errs_on_unexpected_line_id() -> Result<()> {
let mut app = App::new();
app.setup_dialogue_runner().start_node("Start");
app.continue_dialogue_and_update_n_times(4);
app.dialogue_runner_mut()
.select_option_by_line_id(LineId("line:1".to_string()))
.unwrap_err();

Ok(())
}

#[test]
fn option_selection_implies_continue() -> Result<()> {
let mut app = App::new();
Expand Down Expand Up @@ -158,6 +170,35 @@ fn can_select_unavailable_choice() -> Result<()> {
Ok(())
}

#[test]
fn can_select_by_line_id() -> Result<()> {
let mut app = App::new();
let mut asserter = EventAsserter::new();
app.setup_dialogue_runner().start_node("Start");
app.continue_dialogue_and_update_n_times(4);
app.dialogue_runner_mut()
.select_option_by_line_id(LineId("line:x1".to_string()))?;
app.continue_dialogue_and_update();
asserter.clear_events(&mut app);

app.continue_dialogue_and_update();
assert_events!(asserter, app contains [
PresentLineEvent (n = 0),
PresentOptionsEvent with |event|
event.options.len() == 2
&& event.options.iter().filter(|o| o.is_available).count() == 1
&& event.options.iter().filter(|o| !o.is_available).all(|o| o.id == OptionId(0)),
]);
app.dialogue_runner_mut().select_option(OptionId(0))?;
app.update();
assert_events!(asserter, app contains [
PresentLineEvent with |event| event.line.text == lines()[6],
PresentOptionsEvent (n = 0),
]);

Ok(())
}

#[test]
fn generates_files_in_dev_mode() -> Result<()> {
let dir = tempdir()?;
Expand Down
25 changes: 25 additions & 0 deletions crates/runtime/src/dialogue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub enum DialogueError {
selected_option_id: OptionId,
max_id: usize,
},
InvalidLineIdError {
selected_line_id: LineId,
line_ids: Vec<LineId>,
},
UnexpectedOptionSelectionError,
ContinueOnOptionSelectionError,
NoNodeSelectedOnContinue,
Expand Down Expand Up @@ -66,6 +70,10 @@ impl Display for DialogueError {
MarkupParseError(e) => Display::fmt(e, f),
LineProviderError { id, language_code } => write!(f, "Line ID \"{id}\" not found in line provider with language code {language_code:?}"),
InvalidOptionIdError { selected_option_id, max_id } => write!(f, "{selected_option_id:?} is not a valid option ID (expected a number between 0 and {max_id}."),
InvalidLineIdError { selected_line_id, line_ids } => {
let line_ids = line_ids.iter().map(|id| id.0.clone()).collect::<Vec<_>>().join(", ");
write!(f, "{selected_line_id:?} is not a valid line ID of options (expected line ids: {line_ids}.")
},
UnexpectedOptionSelectionError => f.write_str("An option was selected, but the dialogue wasn't waiting for a selection. This method should only be called after the Dialogue is waiting for the user to select an option."),
ContinueOnOptionSelectionError => f.write_str("Dialogue was asked to continue running, but it is waiting for the user to select an option first."),
NoNodeSelectedOnContinue => f.write_str("Cannot continue running dialogue. No node has been selected."),
Expand Down Expand Up @@ -475,6 +483,23 @@ impl Dialogue {
Ok(self)
}

/// Signals to the [`Dialogue`] that the user has selected a specified [`DialogueOption`].
///
/// This makes dialogue replay more robust than [`self.set_selected_option`] when adding new options.
///
/// The ID number that should be passed as the parameter to this method should be the id
/// of the [`line`] field in the [`DialogueOption`] that represents the user's selection.
///
/// ## Panics
/// - If the Dialogue is not expecting an option to be selected.
/// - If the line ID is not found in the vector of [`DialogueOption`] provided by [`DialogueEvent::Options`].
///
/// ## See Also
/// - [`Dialogue::continue_`]
pub fn set_selected_option_by_line_id(&mut self, selected_line_id: LineId) -> Result<OptionId> {
self.vm.set_selected_option_by_line_id(selected_line_id)
}

/// Gets a value indicating whether the Dialogue is currently executing Yarn instructions.
#[must_use]
pub fn is_active(&self) -> bool {
Expand Down
30 changes: 30 additions & 0 deletions crates/runtime/src/virtual_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,36 @@ impl VirtualMachine {
Ok(())
}

pub(crate) fn set_selected_option_by_line_id(
&mut self,
selected_line_id: LineId,
) -> Result<OptionId> {
if self.execution_state != ExecutionState::WaitingOnOptionSelection {
return Err(DialogueError::UnexpectedOptionSelectionError);
}
if let Some(selected_option) = self
.state
.current_options
.iter()
.find(|o| o.line.id == selected_line_id)
{
let selected_option_id = selected_option.id;
self.set_selected_option(selected_option_id)
.map(|_| selected_option_id)
} else {
let line_ids = self
.state
.current_options
.iter()
.map(|o| o.line.id.clone())
.collect();
Err(DialogueError::InvalidLineIdError {
selected_line_id,
line_ids,
})
}
}

pub(crate) fn is_active(&self) -> bool {
self.execution_state != ExecutionState::Stopped
}
Expand Down