Expand description
A pure Rust dialogue system.
Replique reads dialogues written in plain text, compiles them, and runs them
on a small virtual machine. It knows nothing about any engine: the VM hands
out events (say this line, offer these choices, run this command)
and waits for the caller to answer. bevy_replique is the Bevy plugin
built on top of this crate.
§The format
:= start
Alice: Hi! How are you?
-> Good, thanks!
Alice: Glad to hear it.
-> Not great.
Alice: Oh no, what happened?
=> END
---The language is described in the Replique Book, which is the place to look for how a dialogue is written. This documentation is about reading and running one.
§Tags
The #name and #name:value tags of a line reach the caller apart from
its text:
use replique::RepliqueFile;
use replique::dialogue::Tag;
use replique::vm::{DialogueEvent, DialogueVm};
let file = RepliqueFile::from_source(
":= start\nAlice: #angry Give it back! #sound:alice_01\n---\n",
);
let mut vm = DialogueVm::default();
let DialogueEvent::Say { text, tags, .. } =
vm.start(file.dialogue.unwrap(), "start").unwrap()
else {
panic!("expected a line");
};
assert_eq!(text, "Give it back!");
assert_eq!(tags, [Tag::new("angry"), Tag::with_value("sound", "alice_01")]);A choice takes tags the same way, -> #hostile Attack #cost:3, and each
Choice of a DialogueEvent::Choices carries its own.
§Schema
With a RepliqueSchema, the parsed dialogue can be validated against
speaker, tags, functions and commands.
use replique::parser::parse;
use replique::parser::validation::RepliqueSchema;
let schema = RepliqueSchema::from_toml(
r#"
speakers = ["Robin", "Fanny"]
[tags]
sad = { scope = ["Fanny"] }
[commands]
change_mood = "2"
"#,
)
.unwrap();
let mut parsed = parse(
":= start\nFany: Hi!\nRobin: #sad Hello.\n>> change_mood(Fanny)\n---\n",
);
parsed.validate(&schema);
let codes: Vec<_> = parsed.diagnostics.iter().map(|d| d.kind.code()).collect();
assert_eq!(codes, ["unknown-speaker", "tag-out-of-scope", "wrong-arity"]);What a schema finds is a warning: the dialogue still compiles and runs.
Each section is optional and is not checked when empty. The builtins
are known to every schema. See parser::validation for the format of the file.
§Compiling
RepliqueFile is the front door: it parses and compiles a source in one
step, and keeps the Diagnostics either way. A file with errors yields no
Dialogue, so check RepliqueFile::has_errors before running anything.
use replique::RepliqueFile;
use replique::parser::diagnostic::Color;
let file = RepliqueFile::from_source(":= start\nAlice: Hi!\n=> nope\n---\n");
assert!(file.has_errors());
print!("{}", file.render_diagnostics(Color::Never));§Running
DialogueVm is a coroutine: it walks the dialogue until it reaches
something only the caller can handle, then suspends and returns a
DialogueEvent. Answering it with DialogueVm::resume carries on.
use replique::RepliqueFile;
use replique::vm::{DialogueEvent, DialogueVm, ResumeEvent};
let file = RepliqueFile::from_source(
":= start\nAlice: Hi!\n-> Hello\n Alice: Hello!\n-> Bye\n Alice: Bye!\n---\n",
);
let dialogue = file.dialogue.unwrap();
let mut vm = DialogueVm::default();
let mut event = vm.start(dialogue, "start").unwrap();
loop {
event = match event {
DialogueEvent::Say { speaker, text, .. } => {
println!("{}: {}", speaker.unwrap_or_default(), text);
vm.resume(ResumeEvent::Advance).unwrap()
}
// A real game would show these and wait for the player.
DialogueEvent::Choices { .. } => vm.resume(ResumeEvent::Select(0)).unwrap(),
DialogueEvent::Finished => break,
_ => vm.resume(ResumeEvent::Advance).unwrap(),
};
}A Choice whose [if] condition does not hold is still listed, with
Choice::available set to false, so that the indexes do not move.
Selecting it is a VmError::ChoiceNotAvailable.
DialogueVm::vars reads back every variable a run has written.
A run never panics on a malformed dialogue: every way it can go wrong is a
VmError, including the step limit that catches nodes jumping to each
other forever.
§Commands and functions
A >> command(...) does something. It surfaces as
DialogueEvent::Command, which suspends the VM like a line does, so an
animation or a sound can run to completion before the dialogue moves on.
A [function(...)] answers something, and never suspends. The VM resolves
it through the RepliqueHost the caller passes to
DialogueVm::start_with and DialogueVm::resume_with. The same host
answers has when it is asked of a subject rather than of a dictionary,
and gives the value of a feature read with a ., as in Alice.Mood:
use std::collections::HashMap;
use replique::RepliqueFile;
use replique::dialogue::Value;
use replique::host::{HostError, RepliqueHost};
use replique::vm::{DialogueEvent, DialogueVm};
struct Game {
gold: i64,
moods: HashMap<&'static str, &'static str>,
}
impl RepliqueHost for Game {
fn call(&mut self, name: &str, _args: Vec<Value>) -> Result<Value, HostError> {
match name {
"gold" => Ok(Value::Int(self.gold)),
_ => Err(HostError::UnknownFunction(name.into())),
}
}
fn has_feature(&mut self, subject: &str, name: &str) -> Result<bool, HostError> {
if !["Angry", "Happy"].contains(&name) {
return Err(HostError::UnknownFeature(name.into()));
}
match self.moods.get(subject) {
Some(mood) => Ok(*mood == name),
None => Err(HostError::UnknownSubject(subject.into())),
}
}
fn feature(&mut self, subject: &str, name: &str) -> Result<Value, HostError> {
match (self.moods.get(subject), name) {
(Some(mood), "Mood") => Ok(Value::String((*mood).into())),
(Some(_), _) => Err(HostError::UnknownFeature(name.into())),
(None, _) => Err(HostError::UnknownSubject(subject.into())),
}
}
}
let file = RepliqueFile::from_source(
r#":= start
[if Alice has Angry]
Alice: Give me back my [gold()] coins!
[else]
Alice: You have [gold()] coins.
---
"#,
);
let dialogue = file.dialogue.unwrap();
let mut game = Game {
gold: 12,
moods: HashMap::from([("Alice", "Angry")]),
};
let mut vm = DialogueVm::default();
let DialogueEvent::Say { text, .. } = vm.start_with(&mut game, dialogue, "start").unwrap()
else {
panic!("expected a line");
};
assert_eq!(text, "Give me back my 12 coins!");The builtins are available to every dialogue, whatever the host.
§Modules
Modules§
- builtins
- The functions every dialogue can call, whatever the host calling the VM.
- dialogue
- Runnable form of a
.repfile. - host
- Define the
RepliqueHosttrait for the Virtual Machine to execute function on the host side without sending events. - parser
- Parser for Replique
- vm
- Virtual machine executing a compiled
Dialogue.