Introduction
Ayaka project was initially a project for OSPP 2022. It is a voice novel engine which aims at 3 goals: simple, free, concentrate.
Simple
A visual novel should be easy to author. The author need little knowledge of programming. We provide a config file format based on YAML and TeX-like commands. It is easy to learn and author your own voice novel.
Free
You are free to implement anything you'd like with our plugin system. We provide a simple, but also strong script plugin by default. The plugin system provides possibility to calculate and hook important parts in the runtime.
Concentrate
The project is separated into 4 parts: runtime, plugins, config, frontend. You can concentrate at one part without knowing about others. If you like, you can write config file only, and author a voice novel. You can also write your own fantastic game by customizing the frontend.
Start from zero
Here is the guide with zero knowledge of computer programming. You will learn to install Rust and install Makefile. Then you can follow the instructions in Start from source.
Install Rust
Rust is a modern, compiled language, which means it doesn't need a special runtime environment or an interpreter to execute. What we need to install is the development environment of Rust.
Browse to the official cite of Rust first. Then we download following the installation guide on the official cite.
Rust can run on Windows, Linux, macOS, FreeBSD and NetBSD.
If you are working on Windows
Choose to download 32-bit or 64-bit rustup
installer according your operating system.
If you are working on Unix
Run the following command in the terminal
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
and follow the instructions.
After the installation done, type rust --version
, cargo --version
and rustup --version
in the terminal to make sure the installation valid.
Switch to nightly
rustup install nightly
rustup default nightly
Notes on every time before running...
We use nightly toolchain to compile. You need to update regularly
rustup update
and
git pull
Install Makefile
Ayaka uses Makefile to organize the compilation and testing of the components.
If you are working on Windows
We suggest using MSYS2 distributed make
or MSYS2 distributed mingw32-make
, or chocolatey distributed make
.
Install MSYS2 distributed one
Download the installer from MSYS2 official site and follow the installation instructions.
And run the following command in the MSYS2 shell
pacman -S make
to install make
. If you would like a specific MinGW Makefile, e.g. MinGW64 one, run
pacman -S mingw-w64-x86_64-make
to install mingw32-make
in that environment.
Install chocolatey distributed one
Follow the instructions on the chocolatey official site to install chocolatey.
Run the following command
choco install make
Add make
to PATH
environmental variable
If you use MSYS2, make
won't be add into PATH
, and you can only use it in the MSYS2 shell. To make it easier, you can choose to add the corresponding directory of MSYS2 /usr/bin
(or MinGW64 /mingw64/bin
), usually C:\msys64\usr\bin
(or C:\msys64\mingw64\bin
) into environmental variable PATH
.
You can also choose to not changing the environmental variable, but let MSYS2 shell inherit the variables from Windows, to call cargo
and npm
in it. According to the solution from developers, you can change environmental variable MSYS2_PATH_TYPE
to inherit
.
If you are working on Linux
Distros of Linux differ, but usually the package name should be make
.
Start from source
Prerequisites
All platforms need Rust and Nodejs installed.
- Rust: nightly toolchain.
- Nodejs: 14.18+/16+ required by Vite.
tauri-cli
:2.0.0-beta.18
ensure latest beta version, required by Tauri.
$ cargo install tauri-cli --version 2.0.0-beta.18
...
$ cargo tauri --version
tauri-cli 2.0.0-beta.18
Windows
Windows 10+ is recommended but any Windows that Rust supports is OK.
WebView2 is required by Tauri. It comes with the latest Edge browser.
To run the Makefile toolchain, you need GNU Make from MSYS2 project, either for Msys2(make
) or Mingw64(mingw32-make
).
Note that if you have a WSL bash.exe
in PATH before MSYS2 one, the npm
command may fail.
Linux
webkit2gtk
is needed. We only support webkit2gtk-4.0
required by Tauri.
macOS
Generally we don't need anything more, but you should ensure that make
is installed.
Clone from source
$ git clone https://github.com/Uni-Gal/Ayaka.git
$ cd Ayaka
Add targets for WebAssembly
$ rustup target add wasm32-unknown-unknown
Test the utilities
$ make test
Run examples
$ # Run Fibonacci2
$ make example-Fibonacci2
$ # Run Orga in GUI
$ make example-Orga-gui
Release build of frontends
$ make release
Config
Ayaka config is based on YAML. The structure is simple to author.
The articles below uses ayaka-check
to show the example,
it may behave a little different in GUI.
To run the examples, you need to create a config file with full structure, go to the bins
folder and run
$ cargo run --package ayaka-check -- path/to/config.yaml --auto
File structure
Directory structure
config.yaml
├─paras
│ ├─ja
│ │ ├─start.yaml
│ │ └─end.yaml
│ └─zh-Hans
│ ├─start.yaml
│ └─end.yaml
└─res
├─ja.yaml
└─zh-Hans.yaml
Properties
The total config file is a GameConfig
object.
Here shows the properties:
Property | Description |
---|---|
title | The title of the game. |
base_lang | The base language. |
paras | The paragraph path. |
start | The start paragraph. |
author | Optional. The author of the game. |
plugins | Optional. The PluginConfig object. |
res | Optional. The resource path. |
props | Optional. The custom properties. |
The PluginConfig
object contains the base directory and the plugin names:
Property | Description |
---|---|
dir | The directory. |
modules | The plugin names. |
A Paragraph
object is a collection of texts:
Property | Description |
---|---|
tag | The tag and key of the paragraph. |
texts | The texts. |
title | Optional. The title of the paragraph. |
next | Optional. The next paragraph. |
The visibility of paragraphs
Only the paragraph whose tag is the same as the file name(without extension) is public to all paragraphs. The rest paragraphs in this file could only be referenced by the paragraphs in the same file.
For example, for start.yaml
- tag: start
next: foo
- tag: foo
next: bar
- tag: bar
next: end
and end.yaml
- tag: end
next: foo
- tag: foo
next: bar
- tag: bar
The foo
and bar
referenced are the ones in the same file, while start
and end
could be referenced from other files.
Basic example
This is a config example, with 2 paragraphs.
config.yaml
└─paras
└─en
├─para1.yaml
└─para2.yaml
config.yaml
title: Title
base_lang: en
paras: paras
start: para1
para1.yaml
- tag: para1
texts:
- This is the first line.
- This is the second line.
next: para2
para2.yaml
- tag: para2
texts:
- The first line of the second paragraph.
The output will be
This is the first line.
This is the second line.
The first line of the second paragraph.
You can see that the game starts at the first paragraph para1
,
and it jumps to para2
after para1
ends.
The game exits after para2
ends, because it doesn't specify the next paragraph.
Specify character
The dialogues should be marked with the speaking character.
We specify a character with ///
prefix.
Add the character names
The character names should be placed in res
at the front of the game.
Not only because the character name is too long to type again and again, but also to make i18n easy.
The key of the character name should be prefixed with ch_
:
ch_foo: A. Foo
ch_bar: B. Bar
You can then specify the character with the command:
- /foo//This is the first line.
- /bar//This is the second line.
These two lines will output as:
_A. Foo_This is the first line.
_B. Bar_This is the second line.
Note the double slashes in /foo//
could not be simplified.
Specify the alias of the character
Sometimes we need a temporary alias of the current character:
- /foo/Person 1st/This is the first line.
- /bar/Person 2nd/This is the second line.
The output will be
_Person 1st_This is the first line.
_Person 2nd_This is the second line.
Resources
The resources are indexed by locale:
config.yaml
└─res
├─en.yaml
├─ja.yaml
└─zh.yaml
You can specify other locales, too.
The keys not specified in other locales will fallback to base_lang
ones.
en.yaml
foo: Foo
bar: Bar
zh.yaml
foo: 天
bar: 地
Reference resources
You can reference resources in texts with \res{}
command.
- The foo value is \res{foo}
- The bar value is \res{bar}
Internationalization
ICU
The i18n feature are supported by ICU4X with CLDR data. We use CLDR to choose the best fit locale for current system.
Simplify translation
The translation of the texts is always a difficult job. You don't need to copy all commands as is.
For example, the original text (ja
)
- bg: 0
- /rd//団長!車の用意できました!
- switches:
- おう!
- 止まるんじゃねぇぞ!
- 止まれ!
could be translated as (zh-Hans
)
-
- 团长!车已经准备好了!
- switches:
- 哦!
- 不要停下来啊!
- 停下!
Fallback
The resources, commands, and even paragraphs could be fell back, if a translated one is not apparent. However, some other ones couldn't be fell back.
Fallback with empty text
If a certain translated line is empty, it will fall back to the base language one.
Switches
The switches could be specified with switches
command.
Some special global variables are used.
$?
is the index of selected switch (start from 0).
$<num>
indicates whether a switch is enabled, and is cleared after one switch is selected.
If a specific $<num>
variable is not defined or defined as null
or ~
, it is treated as true
.
- 'Choose a switch:'
- exec: $3 = false
- switches:
- Switch 1
- Switch 2
- Not enabled
- You chose switch \var{?}
Script
Ayaka script is dynamic typed.
The only supported types are unit ~
, boolean, integer, and string.
#![allow(unused)] fn main() { pub enum RawValue { Unit, Bool(bool), Num(i64), Str(String), } }
Using ayacript
ayacript
is the plugin that provides Ayaka script functionalities.
You need to add ayascript
to the config file. See Plugin.
Execute scripts
Execute a piece of script(we call it program) with exec
command:
- exec: $res = 1 + 1
- 1 + 1 = \var{res}
The output is
1 + 1 = 2
The script $res = 1 + 1
is evaluated, and the result is 2
.
It is then converted to string and appended to the text.
Example: Fibonacci
With the config file, we can even calculate some math problems. For example, Fibonacci:
- tag: init
texts:
- '1'
- exec: $n = 50; $a = 1; $b = 1; $i = 1;
- \var{b}
next: loop
- tag: loop
texts:
- exec: c = $b; $b += $a; $a = c; $i += 1;
- \var{b}
- exec: $next = if($i < $n, "loop")
next: \var{next}
Runtime
The runtime, aka "backend", is the engine of a game. It provides functionality to load config, settings, plugins, and control the full status.
Run a game
The CLI tool in bins/ayaka-check
is a full example to run a game.
Open a config file
use ayaka_runtime::*;
let mut context = Context::open("../../../examples/Fibonacci/config.yaml", FrontendType::Text).await?;
The context object should be initialized first to start from an initial record.
context.init_new();
Then you can iterate the actions:
while let Some(action) = context.next_run() {
//...
}
Get the open status
The context
also implements Stream
.
The OpenStatus
could be iterated before the future awaited.
use ayaka_runtime::*;
let context = Context::open("../../../examples/Fibonacci/config.yaml", FrontendType::Text);
let mut context = std::pin::pin!(context);
while let Some(status) = context.next().await {
println!("{:?}", status);
}
let mut context = context.await?;
Plugin
All plugins should target WebAssembly.
We now support wasm32-unknown-unknown
target.
The plugin runtime is supported by Wasmer. Our platform support is largely limited by this engine.
We provide a crate ayaka-bindings
to easily author a plugin in Rust.
Load plugins
Specify the plugin directory in the config file:
plugins:
dir: path/to/plugins
The runtime will try to load all WebAssembly file in the directory.
If you want to specify some of them, or specify the load order, specify them in modules
:
plugins:
dir: path/to/plugins
modules:
- foo
- bar
You don't need to specify the extension.
WASM directory mappings
The parent directory of the config file (aka. the root directory) is mapped to /
in the plugins.
Some plugins, e.g. media, need to determine if the resource files exist.
Therefore, the files should be placed under the root directory.
Symbolic links may not work if they point to directories outside the root directory.
The text processing workflow
digraph {
ori[label="Raw text"];
lines[label="Structural lines"];
exec[label="Run scripts"];
cmd[label="Custom commands"];
texts[label="Final texts"];
history[label="Record history"];
output[label="Output"];
ori -> lines [label="TextParser"];
lines -> exec [label="ProgramParser"];
exec -> cmd [label="text plugins"];
cmd -> texts [label="action plugins"];
texts -> history;
history -> output;
}
Script plugin
All plugins are script plugins. Scripts could call methods in the script plugins.
Calling methods
A plugin method is referenced with <module>.<fn>(...)
grammar.
If you would like to call the rnd
function in random
module,
- exec: $i = random.rnd()
Pass the parameters in the brace ()
.
As the scripts are calculated at runtime, if there's no plugin called random
,
or no method called rnd
inside random
, it will give a warning, and continue with RawValue::Unit
.
Author a script plugin
Here we're going to author a script plugin meet
to return a string "Hello".
use ayaka_bindings::*;
#[export]
fn plugin_type() -> PluginType {
PluginType::default()
}
#[export]
fn hello(_args: Vec<RawValue>) -> RawValue {
RawValue::Str("Hello".to_string())
}
And call the function:
- exec: $hello = meet.hello()
- \var{hello} from plugin!
If it builds successfully, and you set the right path to the plugins, it will output:
Hello from plugin!
Existing plugins
There are some existing script (only) plugins:
Plugin | Description |
---|---|
ayalog | Log to runtime. |
random | Generate random numbers. |
Text plugin
Text plugins deal with custom TeX-like commands.
Register commands
Here we will register a command \hello
and call it in the config file.
You also need plugin_type
to specify that it is a text plugin.
use ayaka_bindings::*;
#[export]
fn plugin_type() -> PluginType {
PluginType::builder().text(["hello"]).build()
}
#[export]
fn hello(_args: Vec<String>, _ctx: TextProcessContext) -> TextProcessResult {
let mut res = TextProcessResult::default();
res.line.push_back_chars("Hello");
res
}
Call the command in the config file:
- \hello world!
And it outputs:
Hello world!
The process results
The TextProcessResult
object is some lines and properties to be added to the current action. line
will be appended to the current position of the command, and props
will be set and update.
Existing plugins
Plugin | Description |
---|---|
basictex | Basic TeX commands. |
live2d | Live2D commands. |
Line plugin
Line plugins provides custom line types.
Register commands
Here we will register a command \hello
and call it in the config file.
You also need plugin_type
to specify that it is a line plugin.
use ayaka_bindings::*;
#[export]
fn plugin_type() -> PluginType {
PluginType::builder().line(["hello"]).build()
}
#[export]
fn hello(_ctx: LineProcessContext) -> LineProcessResult {
let mut res = LineProcessResult::default();
res.locals.insert("hello".to_string(), RawValue::Str("Hello".to_string()));
res
}
Call the command in the config file:
- hello:
- \var{hello} world!
And it outputs:
Hello world!
The process results
The LineProcessResult
object contains the global variables and temp variables. The temp variables will only apply to this specific line.
Existing plugins
Plugin | Description |
---|---|
ayacript | Ayaka script. |
media | Multimedia commands. |
Action plugin
Action plugins deal with the whole action after the text being parsed. An action plugin usually modifies the text, for example, parses the Markdown text and output to HTML.
Action plugins could access the last action in the history, will text plugins cannot.
Simple Markdown plugin
Action plugins don't need to register anything, but only to specify in the plugin_type
:
use ayaka_bindings::*;
use pulldown_cmark::*;
#[export]
fn plugin_type() -> PluginType {
PluginType::builder().action().build()
}
#[export]
fn process_action(mut ctx: ActionProcessContext) -> ActionProcessResult {
let line = ctx
.action
.line
.into_iter()
.map(|s| s.into_string())
.collect::<Vec<_>>()
.concat();
let parser = Parser::new(&line);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
ctx.action.clear();
ctx.action.push_back_chars(html_output);
ActionProcessResult { action: ctx.action }
}
Add the plugin to the modules
list, and all markdown text will be translated to HTML.
You may notice that the HTML tags are treated as ActionLine::Chars
, which means they will be displayed one by one on GUI frontends. Our existing markdown
plugin resolves this problem by providing a custom writer.
Existing plugins
Plugin | Description |
---|---|
live2d | Inherit models layout and deal with hiding. |
markdown | Process Markdown texts. |
media | Voice of each action. |
Game plugin
Game plugins adjust some properties of the game before any record starts.
Insert a global property
use ayaka_bindings::*;
#[export]
fn plugin_type() -> PluginType {
PluginType::builder().game().build()
}
#[export]
fn process_game(mut ctx: GameProcessContext) -> GameProcessResult {
ctx.props.insert("hello".to_string(), "Hello world!".to_string());
GameProcessResult { props: ctx.props }
}
Existing plugins
Plugin | Description |
---|---|
live2d | Get correct path of Live2D models. |
media | Get correct path of background image at home. |
GUI
The ayaka-gui
is a sample GUI frontend.
It provides basic functionalities, including control flow, multimedia, i18n and Live2D integration.
The frontend is based on Tauri with Vue.
Live2D
The Live2D functionality is powered by pixi-live2d-display
.
Packaging
We support package format called ayapack
.
Internally it is a TAR format archive, without any compression.
We load the package file with memory map to reduce allocations.
Packaging
tar
executable is pre-installed on almost all platforms. There may be BSD-Tar or GNU-Tar. For a directory foo
that contains config.yaml
, we can simply execute
$ cd foo
$ tar -cf foo.ayapack *
The parameter c
means creating a package, and f
means the following parameter is the package path.
Details
The details of the parsing and loading are in the vfs-tar
.
Supported platforms
Ayaka is cross-platform, and the triples are supported with 3 tiers.
Tier 1
Ensured to work well with these triples:
- x86_64-pc-windows-msvc
- x86_64-unknown-linux-gnu
- x86_64-apple-darwin
Tier 2
Should work well with these triples, but not tested:
- i686-pc-windows-msvc
- aarch64-pc-windows-msvc
- i686-pc-windows-gnu
- x86_64-pc-windows-gnu
- aarch64-unknown-linux-gnu
- aarch64-apple-darwin
Tier 3
May not build or run because of dependencies:
- s390x-unknown-linux-gnu