diff --git a/bins/Cargo.lock b/bins/Cargo.lock index 2db9e751..5a019169 100644 --- a/bins/Cargo.lock +++ b/bins/Cargo.lock @@ -198,7 +198,7 @@ name = "ayaka-gui" version = "0.1.0" dependencies = [ "axum", - "ayaka-runtime", + "ayaka-model", "flexi_logger", "mime_guess", "serde", @@ -209,7 +209,6 @@ dependencies = [ "tauri-plugin-window-state", "tower-http", "tryiterator", - "trylog", ] [[package]] @@ -222,6 +221,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "ayaka-model" +version = "0.1.0" +dependencies = [ + "ayaka-runtime", + "serde", + "stream-future", + "tryiterator", + "trylog", +] + [[package]] name = "ayaka-plugin" version = "0.1.0" @@ -275,7 +285,6 @@ dependencies = [ "log", "rand 0.8.5", "serde", - "serde_json", "serde_yaml", "stream-future", "sys-locale", diff --git a/bins/Cargo.toml b/bins/Cargo.toml index b7568913..63de57e9 100644 --- a/bins/Cargo.toml +++ b/bins/Cargo.toml @@ -14,6 +14,7 @@ opt-level = "z" [workspace.dependencies] ayaka-runtime = { path = "../utils/ayaka-runtime" } +ayaka-model = { path = "../utils/ayaka-model" } tokio = { version = "1" } clap = { version = "4.0" } flexi_logger = { version = "0.24", default-features = false, features = ["colors"] } diff --git a/bins/ayaka-check/src/main.rs b/bins/ayaka-check/src/main.rs index 1786e33e..c0b70074 100644 --- a/bins/ayaka-check/src/main.rs +++ b/bins/ayaka-check/src/main.rs @@ -57,7 +57,7 @@ async fn main() -> Result<()> { } } let mut ctx = context.await?; - ctx.init_new(); + ctx.set_start_context(); let loc = opts.locale.unwrap_or_else(Locale::current); while let Some(raw_ctx) = ctx.next_run() { let action = ctx.get_action(&loc, &raw_ctx)?; diff --git a/bins/ayaka-gui/src-tauri/Cargo.toml b/bins/ayaka-gui/src-tauri/Cargo.toml index be3c14e9..4d7a546c 100644 --- a/bins/ayaka-gui/src-tauri/Cargo.toml +++ b/bins/ayaka-gui/src-tauri/Cargo.toml @@ -7,13 +7,12 @@ edition = "2021" tauri-build = { version = "1.0", features = [] } [dependencies] -ayaka-runtime = { workspace = true } +ayaka-model = { workspace = true } flexi_logger = { workspace = true } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2", features = ["cli", "window-all"] } tauri-plugin-window-state = "0.1" -trylog = "0.3" tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" } axum = { version = "0.6", default-features = false, features = ["http1", "tokio"] } tower-http = { version = "0.3", features = ["cors"] } diff --git a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs index 7adffd4f..6866530e 100644 --- a/bins/ayaka-gui/src-tauri/src/asset_resolver.rs +++ b/bins/ayaka-gui/src-tauri/src/asset_resolver.rs @@ -6,7 +6,7 @@ use axum::{ routing::get, Router, Server, }; -use ayaka_runtime::{log, vfs::*}; +use ayaka_model::{log, vfs::*}; use std::{ io::{Read, Result}, net::TcpListener, diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs index a0fc94e2..86eb8b27 100644 --- a/bins/ayaka-gui/src-tauri/src/main.rs +++ b/bins/ayaka-gui/src-tauri/src/main.rs @@ -9,10 +9,8 @@ mod asset_resolver; mod settings; -use ayaka_runtime::{ +use ayaka_model::{ anyhow::{self, Result}, - log::{debug, info}, - settings::*, *, }; use flexi_logger::{FileSpec, LogSpecification, Logger}; @@ -24,10 +22,9 @@ use std::{ net::TcpListener, }; use tauri::{ - async_runtime::Mutex, command, utils::config::AppUrl, AppHandle, Manager, PathResolver, State, + async_runtime::RwLock, command, utils::config::AppUrl, AppHandle, Manager, PathResolver, State, WindowUrl, }; -use trylog::macros::*; type CommandResult = Result; @@ -52,49 +49,22 @@ impl Display for CommandError { #[command] fn ayaka_version() -> &'static str { - ayaka_runtime::version() + ayaka_model::version() } -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "t", content = "data")] -enum OpenGameStatus { - LoadProfile, - CreateRuntime, - LoadPlugin(String, usize, usize), - GamePlugin, - LoadResource, - LoadParagraph, - LoadSettings, - LoadGlobalRecords, - LoadRecords, - Loaded, -} - -impl OpenGameStatus { - pub fn emit(self, handle: &AppHandle) -> Result<(), tauri::Error> { - handle.emit_all("ayaka://open_status", self) - } -} - -#[derive(Default)] struct Storage { config: Vec, dist_port: u16, - manager: FileSettingsManager, - records: Mutex>, - context: Mutex>, - current: Mutex>, - settings: Mutex>, - global_record: Mutex>, + model: RwLock>, } impl Storage { pub fn new(resolver: &PathResolver, config: Vec, dist_port: u16) -> Self { + let manager = FileSettingsManager::new(resolver); Self { config, dist_port, - manager: FileSettingsManager::new(resolver), - ..Default::default() + model: RwLock::new(GameViewModel::new(manager)), } } } @@ -124,115 +94,51 @@ fn dist_port(storage: State) -> u16 { #[command] async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandResult<()> { let config = &storage.config; - let context = Context::open(config, FrontendType::Html); - pin_mut!(context); - while let Some(status) = context.next().await { - match status { - OpenStatus::LoadProfile => { - OpenGameStatus::LoadProfile.emit(&handle)?; - } - OpenStatus::CreateRuntime => OpenGameStatus::CreateRuntime.emit(&handle)?, - OpenStatus::LoadPlugin(name, i, len) => { - OpenGameStatus::LoadPlugin(name, i, len).emit(&handle)? - } - OpenStatus::GamePlugin => OpenGameStatus::GamePlugin.emit(&handle)?, - OpenStatus::LoadResource => OpenGameStatus::LoadResource.emit(&handle)?, - OpenStatus::LoadParagraph => OpenGameStatus::LoadParagraph.emit(&handle)?, + let mut model = storage.model.write().await; + { + let context = model.open_game(config, FrontendType::Html); + pin_mut!(context); + while let Some(status) = context.next().await { + handle.emit_all("ayaka://open_status", status)?; } + context.await?; } - let ctx = context.await?; + asset_resolver::ROOT_PATH - .set(ctx.root_path.clone()) + .set(model.context().root_path().clone()) .unwrap(); let window = handle.get_window("main").unwrap(); - window.set_title(&ctx.game.config.title)?; - let settings = { - OpenGameStatus::LoadSettings.emit(&handle)?; - unwrap_or_default_log!(storage.manager.load_settings(), "Load settings failed") - }; - *storage.settings.lock().await = Some(settings); - - OpenGameStatus::LoadGlobalRecords.emit(&handle)?; - let global_record = unwrap_or_default_log!( - storage.manager.load_global_record(&ctx.game.config.title), - "Load global records failed" - ); - *storage.global_record.lock().await = Some(global_record); - - OpenGameStatus::LoadRecords.emit(&handle)?; - *storage.records.lock().await = unwrap_or_default_log!( - storage.manager.load_records(&ctx.game.config.title), - "Load records failed" - ); - *storage.context.lock().await = Some(ctx); - - OpenGameStatus::Loaded.emit(&handle)?; + window.set_title(&model.context().game().config.title)?; + Ok(()) } #[command] -async fn get_settings(storage: State<'_, Storage>) -> CommandResult> { - Ok(storage.settings.lock().await.clone()) +async fn get_settings(storage: State<'_, Storage>) -> CommandResult { + Ok(storage.model.read().await.settings().clone()) } #[command] async fn set_settings(settings: Settings, storage: State<'_, Storage>) -> CommandResult<()> { - *storage.settings.lock().await = Some(settings); + storage.model.write().await.set_settings(settings); Ok(()) } #[command] async fn get_records(storage: State<'_, Storage>) -> CommandResult> { - let context = storage.context.lock().await; - let context = context.as_ref().unwrap(); - let settings = storage.settings.lock().await; - let settings = settings.as_ref().unwrap(); - let mut res = vec![]; - for record in storage.records.lock().await.iter() { - let raw_ctx = record.history.last().unwrap(); - let action = context.get_action(&settings.lang, raw_ctx)?; - if let Action::Text(action) = action { - res.push(action); - } else { - unreachable!() - } - } - Ok(res) + Ok(storage.model.read().await.records_text().collect()) } #[command] async fn save_record_to(index: usize, storage: State<'_, Storage>) -> CommandResult<()> { - let mut records = storage.records.lock().await; - let record = storage - .context - .lock() - .await - .as_ref() - .unwrap() - .record - .clone(); - if index >= records.len() { - records.push(record); - } else { - records[index] = record; - } + storage.model.write().await.save_current_to(index); Ok(()) } #[command] async fn save_all(storage: State<'_, Storage>) -> CommandResult<()> { - let context = storage.context.lock().await; - let game = &context.as_ref().unwrap().game.config.title; - storage - .manager - .save_settings(storage.settings.lock().await.as_ref().unwrap())?; - storage - .manager - .save_global_record(game, storage.global_record.lock().await.as_ref().unwrap())?; - storage - .manager - .save_records(game, &storage.records.lock().await)?; + storage.model.read().await.save_settings()?; Ok(()) } @@ -241,18 +147,16 @@ async fn avaliable_locale( storage: State<'_, Storage>, locales: HashSet, ) -> CommandResult> { - let avaliable = storage - .context - .lock() + Ok(storage + .model + .read() .await - .as_ref() - .unwrap() - .game - .paras - .keys() + .avaliable_locale() .cloned() - .collect(); - Ok(locales.intersection(&avaliable).cloned().collect()) + .collect::>() + .intersection(&locales) + .cloned() + .collect()) } #[command] @@ -262,73 +166,46 @@ async fn choose_locale( ) -> CommandResult> { let locales = avaliable_locale(storage, locales).await?; let current = Locale::current(); - debug!("Choose {} from {:?}", current, locales); + log::debug!("Choose {} from {:?}", current, locales); Ok(current.choose_from(&locales).cloned()) } #[command] async fn info(storage: State<'_, Storage>) -> CommandResult> { - let ctx = storage.context.lock().await; - Ok(Some(GameInfo::new(&ctx.as_ref().unwrap().game))) + Ok(Some(GameInfo::new( + storage.model.read().await.context().game(), + ))) } #[command] -async fn start_new(locale: Locale, storage: State<'_, Storage>) -> CommandResult<()> { - storage.context.lock().await.as_mut().unwrap().init_new(); - info!("Init new context with locale {}.", locale); +async fn start_new(storage: State<'_, Storage>) -> CommandResult<()> { + storage.model.write().await.init_new(); Ok(()) } #[command] -async fn start_record( - locale: Locale, - index: usize, - storage: State<'_, Storage>, -) -> CommandResult<()> { - let record = storage.records.lock().await[index].clone(); - storage - .context - .lock() - .await - .as_mut() - .unwrap() - .init_context(record); - info!("Init new context with locale {}.", locale); +async fn start_record(index: usize, storage: State<'_, Storage>) -> CommandResult<()> { + storage.model.write().await.init_context_by_index(index); Ok(()) } #[command] async fn next_run(storage: State<'_, Storage>) -> CommandResult { loop { - let mut context = storage.context.lock().await; - let context = context.as_mut().unwrap(); - if let Some(raw_ctx) = context.next_run() { - debug!("Next action: {:?}", raw_ctx); + let mut model = storage.model.write().await; + if model.next_run() { let is_empty = { - let action = context.get_action(&context.game.config.base_lang, &raw_ctx)?; - if let Action::Empty = action { - true - } else if let Action::Custom(vars) = action { - // Scripts will also update temp variables. - // Only when the video is set will the method return. - !vars.contains_key("video") - } else { - false + let action = model.current_action().unwrap(); + match action { + Action::Empty => true, + Action::Custom(vars) => !vars.contains_key("video"), + _ => false, } }; - storage - .global_record - .lock() - .await - .as_mut() - .unwrap() - .update(&raw_ctx); - *storage.current.lock().await = Some(raw_ctx); if !is_empty { return Ok(true); } } else { - *storage.current.lock().await = None; return Ok(false); } } @@ -336,104 +213,40 @@ async fn next_run(storage: State<'_, Storage>) -> CommandResult { #[command] async fn next_back_run(storage: State<'_, Storage>) -> CommandResult { - let mut context = storage.context.lock().await; - let context = context.as_mut().unwrap(); - if let Some(raw_ctx) = context.next_back_run() { - debug!("Last action: {:?}", raw_ctx); - *storage.current.lock().await = Some(raw_ctx.clone()); - Ok(true) - } else { - debug!("No action in the history."); - Ok(false) - } + Ok(storage.model.write().await.next_back_run()) } #[command] async fn current_visited(storage: State<'_, Storage>) -> CommandResult { - let raw_ctx = storage.current.lock().await; - let visited = if let Some(raw_ctx) = raw_ctx.as_ref() { - let record = storage.global_record.lock().await; - record.as_ref().unwrap().visited(raw_ctx) - } else { - false - }; - Ok(visited) + Ok(storage.model.read().await.current_visited()) } #[command] async fn current_run(storage: State<'_, Storage>) -> CommandResult> { - let raw_ctx = storage.current.lock().await; - Ok(raw_ctx.as_ref().cloned()) -} - -fn get_actions( - context: &Context, - settings: &Settings, - raw_ctx: &RawContext, -) -> (Action, Option) { - let action = unwrap_or_default_log!( - context.get_action(&settings.lang, raw_ctx), - "Cannot get action" - ); - let base_action = settings.sub_lang.as_ref().map(|sub_lang| { - unwrap_or_default_log!( - context.get_action(sub_lang, raw_ctx), - "Cannot get sub action" - ) - }); - (action, base_action) + Ok(storage.model.read().await.current_run().cloned()) } #[command] async fn current_action( storage: State<'_, Storage>, ) -> CommandResult)>> { - let context = storage.context.lock().await; - let context = context.as_ref().unwrap(); - let raw_ctx = storage.current.lock().await; - let settings = storage.settings.lock().await; - let settings = settings.as_ref().unwrap(); - Ok(raw_ctx - .as_ref() - .map(|raw_ctx| get_actions(context, settings, raw_ctx))) + Ok(storage.model.read().await.current_actions()) } #[command] async fn current_title(storage: State<'_, Storage>) -> CommandResult> { - let settings = storage.settings.lock().await; - let settings = settings.as_ref().unwrap(); - Ok(storage - .context - .lock() - .await - .as_ref() - .unwrap() - .current_paragraph_title(&settings.lang) - .cloned()) + Ok(storage.model.read().await.current_title().cloned()) } #[command] async fn switch(i: usize, storage: State<'_, Storage>) -> CommandResult<()> { - debug!("Switch {}", i); - storage.context.lock().await.as_mut().unwrap().switch(i); + storage.model.write().await.switch(i); Ok(()) } #[command] async fn history(storage: State<'_, Storage>) -> CommandResult)>> { - let context = storage.context.lock().await; - let context = context.as_ref().unwrap(); - let settings = storage.settings.lock().await; - let settings = settings.as_ref().unwrap(); - let mut hs = context - .record - .history - .iter() - .map(|raw_ctx| get_actions(context, settings, raw_ctx)) - .collect::>(); - hs.reverse(); - debug!("Get history {:?}", hs); - Ok(hs) + Ok(storage.model.read().await.current_history().rev().collect()) } fn main() -> Result<()> { diff --git a/bins/ayaka-gui/src-tauri/src/settings.rs b/bins/ayaka-gui/src-tauri/src/settings.rs index 8791d11f..6b0a22e0 100644 --- a/bins/ayaka-gui/src-tauri/src/settings.rs +++ b/bins/ayaka-gui/src-tauri/src/settings.rs @@ -1,6 +1,6 @@ -use ayaka_runtime::{ +use ayaka_model::{ anyhow::{self, Result}, - settings::*, + *, }; use serde::{de::DeserializeOwned, Serialize}; use std::path::{Path, PathBuf}; diff --git a/bins/ayaka-gui/src/interop/index.ts b/bins/ayaka-gui/src/interop/index.ts index e4c3b2a6..ffbae67c 100644 --- a/bins/ayaka-gui/src/interop/index.ts +++ b/bins/ayaka-gui/src/interop/index.ts @@ -103,7 +103,7 @@ export function open_game(): Promise { return invoke("open_game") } -export function get_settings(): Promise { +export function get_settings(): Promise { return invoke("get_settings") } @@ -120,13 +120,13 @@ export function save_record_to(index: number): Promise { } export async function set_locale(loc: Locale): Promise { - let settings = await get_settings() ?? { lang: "" }; + let settings = await get_settings() settings.lang = loc await set_settings(settings) } export async function set_sub_locale(loc?: Locale): Promise { - let settings = await get_settings() ?? { lang: "" }; + let settings = await get_settings() settings.sub_lang = loc await set_settings(settings) } @@ -148,12 +148,12 @@ export async function info(): Promise { return res ?? { title: "", author: "", props: {} } } -export function start_new(locale: Locale): Promise { - return invoke("start_new", { locale: locale }) +export function start_new(): Promise { + return invoke("start_new", {}) } -export function start_record(locale: Locale, index: number): Promise { - return invoke("start_record", { locale: locale, index: index }) +export function start_record(index: number): Promise { + return invoke("start_record", { index: index }) } export function next_run(): Promise { diff --git a/bins/ayaka-gui/src/views/HomeView.vue b/bins/ayaka-gui/src/views/HomeView.vue index 4837c03e..7a2792e2 100644 --- a/bins/ayaka-gui/src/views/HomeView.vue +++ b/bins/ayaka-gui/src/views/HomeView.vue @@ -18,7 +18,7 @@ export default { }, methods: { async new_game() { - await start_new(this.$i18n.locale) + await start_new() if (await next_run()) { this.$router.replace("/game") } diff --git a/bins/ayaka-gui/src/views/RecordsView.vue b/bins/ayaka-gui/src/views/RecordsView.vue index f98570f1..a0ad0c22 100644 --- a/bins/ayaka-gui/src/views/RecordsView.vue +++ b/bins/ayaka-gui/src/views/RecordsView.vue @@ -18,7 +18,7 @@ export default { methods: { async on_record_click(index: number) { if (this.op == "load") { - await start_record(this.$i18n.locale, index) + await start_record(index) await this.$router.replace("/game") } else if (this.op == "save") { await save_record_to(index) diff --git a/bins/ayaka-gui/src/views/SettingsView.vue b/bins/ayaka-gui/src/views/SettingsView.vue index 0c3bea2e..1bf9526a 100644 --- a/bins/ayaka-gui/src/views/SettingsView.vue +++ b/bins/ayaka-gui/src/views/SettingsView.vue @@ -19,7 +19,7 @@ export default { }, async created() { this.locales = await avaliable_locale(this.$i18n.availableLocales) - let sub_locale = (await get_settings())?.sub_lang + let sub_locale = (await get_settings()).sub_lang if (sub_locale && this.$i18n.locale != sub_locale) { this.sub_locale = sub_locale } diff --git a/bins/ayaka-gui/src/views/StartView.vue b/bins/ayaka-gui/src/views/StartView.vue index 07ebb1c6..8fc1682d 100644 --- a/bins/ayaka-gui/src/views/StartView.vue +++ b/bins/ayaka-gui/src/views/StartView.vue @@ -92,7 +92,7 @@ export default { async process_settings() { const settings = await get_settings() console.log(settings) - let loc = settings?.lang + let loc: string | undefined = settings.lang if (!loc || !this.$i18n.availableLocales.includes(loc)) { loc = await choose_locale(this.$i18n.availableLocales) } diff --git a/bins/ayaka-latex/src/main.rs b/bins/ayaka-latex/src/main.rs index d345ff20..66787526 100644 --- a/bins/ayaka-latex/src/main.rs +++ b/bins/ayaka-latex/src/main.rs @@ -36,14 +36,16 @@ async fn main() -> Result<()> { output.command("usepackage", ["lua-ul"]).await?; output.command("usepackage", ["luatexja-ruby"]).await?; output.command("usepackage", ["verbatim"]).await?; - output.command("title", [&ctx.game.config.title]).await?; - output.command("author", [&ctx.game.config.author]).await?; + output.command("title", [&ctx.game().config.title]).await?; + output + .command("author", [&ctx.game().config.author]) + .await?; output .environment("document", |output| async move { output.command0("maketitle").await?; output.command0("tableofcontents").await?; - ctx.init_new(); + ctx.set_start_context(); let loc = opts.locale.unwrap_or_else(Locale::current); let mut current_para = None; diff --git a/utils/Cargo.toml b/utils/Cargo.toml index ca1ed53d..8584b770 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,6 +9,7 @@ members = [ "ayaka-plugin-wasmtime", "ayaka-plugin-wasmi", "ayaka-runtime", + "ayaka-model", ] resolver = "2" @@ -22,6 +23,7 @@ ayaka-plugin-nop = { path = "ayaka-plugin-nop" } ayaka-plugin-wasmer = { path = "ayaka-plugin-wasmer" } ayaka-plugin-wasmtime = { path = "ayaka-plugin-wasmtime" } ayaka-plugin-wasmi = { path = "ayaka-plugin-wasmi" } +ayaka-runtime = { path = "ayaka-runtime" } anyhow = "1.0" @@ -30,9 +32,12 @@ fallback = "0.1" log = "0.4" trylog = "0.3" +tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" } + +stream-future = "0.3" + rmp-serde = "1.1" serde = "1.0" -serde_json = "1.0" serde_with = "2.0" serde_yaml = "0.9" diff --git a/utils/ayaka-model/Cargo.toml b/utils/ayaka-model/Cargo.toml new file mode 100644 index 00000000..dac62375 --- /dev/null +++ b/utils/ayaka-model/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ayaka-model" +version = "0.1.0" +edition = "2021" + +[dependencies] +ayaka-runtime = { workspace = true } +serde = { workspace = true, features = ["derive"] } +stream-future = { workspace = true } +trylog = { workspace = true } +tryiterator = { workspace = true } diff --git a/utils/ayaka-model/src/lib.rs b/utils/ayaka-model/src/lib.rs new file mode 100644 index 00000000..a81a070a --- /dev/null +++ b/utils/ayaka-model/src/lib.rs @@ -0,0 +1,19 @@ +//! The high level wrapper model of Ayaka. +//! +//! This crate provides a view model for a full-functionality frontend, +//! and a abstract trait of settings manager. +//! +//! It re-exports the types of [`ayaka_runtime`]. + +#![warn(missing_docs)] +#![deny(unsafe_code)] +#![feature(generators)] + +mod settings; +pub use settings::*; + +mod view_model; +pub use view_model::*; + +#[doc(no_inline)] +pub use ayaka_runtime::*; diff --git a/utils/ayaka-runtime/src/settings.rs b/utils/ayaka-model/src/settings.rs similarity index 94% rename from utils/ayaka-runtime/src/settings.rs rename to utils/ayaka-model/src/settings.rs index 23d25fab..765344bc 100644 --- a/utils/ayaka-runtime/src/settings.rs +++ b/utils/ayaka-model/src/settings.rs @@ -2,7 +2,6 @@ use crate::*; use anyhow::Result; -use ayaka_bindings_types::RawContext; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ collections::HashMap, @@ -64,11 +63,9 @@ impl ActionRecord { /// Get the [`RawContext`] object from the last [`Action`] in the history, /// and if the history is empty, create a new [`RawContext`] from the game. pub fn last_ctx_with_game(&self, game: &Game) -> RawContext { - self.last_ctx().cloned().unwrap_or_else(|| RawContext { - cur_base_para: game.config.start.clone(), - cur_para: game.config.start.clone(), - ..Default::default() - }) + self.last_ctx() + .cloned() + .unwrap_or_else(|| game.start_context()) } } diff --git a/utils/ayaka-model/src/view_model.rs b/utils/ayaka-model/src/view_model.rs new file mode 100644 index 00000000..b47c5e2e --- /dev/null +++ b/utils/ayaka-model/src/view_model.rs @@ -0,0 +1,326 @@ +use crate::*; +use anyhow::Result; +use serde::Serialize; +use std::path::Path; +use stream_future::stream; +use trylog::macros::*; + +/// The status when calling [`open_game`]. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "t", content = "data")] +pub enum OpenGameStatus { + /// Start loading config file. + LoadProfile, + /// Start creating plugin runtime. + CreateRuntime, + /// Loading the plugin. + LoadPlugin(String, usize, usize), + /// Executing game plugins. + GamePlugin, + /// Loading the resources. + LoadResource, + /// Loading the paragraphs. + LoadParagraph, + /// Loading the settings. + LoadSettings, + /// Loading the global records. + LoadGlobalRecords, + /// Loading the records. + LoadRecords, + /// The game is loaded. + Loaded, +} + +impl From for OpenGameStatus { + fn from(value: OpenStatus) -> Self { + match value { + OpenStatus::LoadProfile => Self::LoadProfile, + OpenStatus::CreateRuntime => Self::CreateRuntime, + OpenStatus::LoadPlugin(name, i, len) => Self::LoadPlugin(name, i, len), + OpenStatus::GamePlugin => Self::GamePlugin, + OpenStatus::LoadResource => Self::LoadResource, + OpenStatus::LoadParagraph => Self::LoadParagraph, + } + } +} + +/// A view model of Ayaka. +/// It manages all settings and provides high-level APIs. +pub struct GameViewModel { + context: Option, + current_record: ActionRecord, + current_raw_context: Option, + settings_manager: M, + settings: Option, + records: Vec, + global_record: Option, +} + +impl GameViewModel { + /// Create a [`GameViewModel`] with a settings manager. + pub fn new(settings_manager: M) -> Self { + Self { + settings_manager, + context: None, + current_record: ActionRecord::default(), + current_raw_context: None, + settings: None, + records: vec![], + global_record: None, + } + } + + /// Open the game with paths and frontend type. + #[stream(OpenGameStatus, lifetime = "'a")] + pub async fn open_game<'a>( + &'a mut self, + paths: &'a [impl AsRef], + frontend_type: FrontendType, + ) -> Result<()> { + let context = Context::open(paths, frontend_type); + pin_mut!(context); + while let Some(status) = context.next().await { + yield status.into(); + } + let context = context.await?; + + let settings = { + yield OpenGameStatus::LoadSettings; + unwrap_or_default_log!( + self.settings_manager.load_settings(), + "Load settings failed" + ) + }; + self.settings = Some(settings); + + yield OpenGameStatus::LoadGlobalRecords; + let global_record = unwrap_or_default_log!( + self.settings_manager + .load_global_record(&context.game().config.title), + "Load global records failed" + ); + self.global_record = Some(global_record); + + yield OpenGameStatus::LoadRecords; + self.records = unwrap_or_default_log!( + self.settings_manager + .load_records(&context.game().config.title), + "Load records failed" + ); + self.context = Some(context); + + yield OpenGameStatus::Loaded; + + Ok(()) + } + + /// The [`Context`], should be called after [`open_game`]. + pub fn context(&self) -> &Context { + self.context.as_ref().unwrap() + } + + /// The [`Context`], should be called after [`open_game`]. + pub fn context_mut(&mut self) -> &mut Context { + self.context.as_mut().unwrap() + } + + /// The current [`ActionRecord`]. + pub fn record(&self) -> &ActionRecord { + &self.current_record + } + + /// The loaded [`Settings`]. + pub fn settings(&self) -> &Settings { + self.settings.as_ref().unwrap() + } + + /// Set the [`Settings`]. + pub fn set_settings(&mut self, settings: Settings) { + self.settings = Some(settings); + } + + /// The loaded [`ActionRecord`]s. + pub fn records(&self) -> &[ActionRecord] { + &self.records + } + + /// The loaded [`GlobalRecord`]. + pub fn global_record(&self) -> &GlobalRecord { + self.global_record.as_ref().unwrap() + } + + /// Get the avaliable locales from paragraphs. + pub fn avaliable_locale(&self) -> impl Iterator { + self.context().game().paras.keys() + } + + /// Start a new game. + pub fn init_new(&mut self) { + self.init_context(ActionRecord { history: vec![] }) + } + + /// Start a game with record. + pub fn init_context(&mut self, record: ActionRecord) { + let ctx = record.last_ctx_with_game(self.context().game()); + self.current_record = record; + log::debug!("Context: {:?}", ctx); + self.context_mut().set_context(ctx) + } + + /// Start a game with the index of records. + pub fn init_context_by_index(&mut self, index: usize) { + self.init_context(self.records()[index].clone()) + } + + fn push_history(&mut self, ctx: &RawContext) { + let cur_text = self + .context() + .game() + .find_para( + &self.context().game().config.base_lang, + &ctx.cur_base_para, + &ctx.cur_para, + ) + .and_then(|p| p.texts.get(ctx.cur_act)); + let is_text = cur_text + .map(|line| matches!(line, Line::Text(_))) + .unwrap_or_default(); + if is_text { + self.current_record.history.push(ctx.clone()); + } + } + + /// Step to the next run. + pub fn next_run(&mut self) -> bool { + let ctx = self.context_mut().next_run(); + if let Some(ctx) = &ctx { + self.push_history(ctx); + self.global_record.as_mut().unwrap().update(ctx); + log::debug!("{:?}", ctx); + } + self.current_raw_context = ctx; + self.current_raw_context.is_some() + } + + /// Step back to the last run. + pub fn next_back_run(&mut self) -> bool { + if self.current_record.history.len() <= 1 { + log::debug!("No action in the history."); + false + } else { + // The last entry is the current one. + // We don't assume that a user could call next_back_run when the + // current run is empty. + self.current_record.history.pop(); + // When we pop the current run, the last entry is what we want. + self.current_raw_context = self.current_record.history.last().cloned(); + debug_assert!(self.current_raw_context.is_some()); + // We clone the (new) current run to set the "next" raw context. + // We don't use the popped run to set the raw context, + // because the empty runs are not recorded. + let mut ctx = self.current_raw_context.clone().unwrap(); + ctx.cur_act += 1; + self.context_mut().set_context(ctx); + true + } + } + + /// Get the current [`RawContext`]. + pub fn current_run(&self) -> Option<&RawContext> { + self.current_raw_context.as_ref() + } + + /// Get the current paragraph title. + pub fn current_title(&self) -> Option<&String> { + self.context() + .current_paragraph_title(&self.settings().lang) + } + + /// Get the current action by language. + pub fn current_action(&self) -> Option { + self.current_run().map(|raw_ctx| { + unwrap_or_default_log!( + self.context().get_action(&self.settings().lang, raw_ctx), + "Cannot get action" + ) + }) + } + + /// Get the current action by language and secondary language. + pub fn current_actions(&self) -> Option<(Action, Option)> { + self.current_run().map(|raw_ctx| self.get_actions(raw_ctx)) + } + + fn get_actions(&self, raw_ctx: &RawContext) -> (Action, Option) { + let action = unwrap_or_default_log!( + self.context().get_action(&self.settings().lang, raw_ctx), + "Cannot get action" + ); + let base_action = self.settings().sub_lang.as_ref().map(|sub_lang| { + unwrap_or_default_log!( + self.context().get_action(sub_lang, raw_ctx), + "Cannot get sub action" + ) + }); + (action, base_action) + } + + /// Choose a switch item by index. + pub fn switch(&mut self, i: usize) { + log::debug!("Switch {}", i); + self.context_mut().switch(i); + } + + /// Save current [`ActionRecord`] to the records. + pub fn save_current_to(&mut self, index: usize) { + let record = self.current_record.clone(); + if index >= self.records.len() { + self.records.push(record); + } else { + self.records[index] = record; + } + } + + /// Save all settings and records. + pub fn save_settings(&self) -> Result<()> { + let game = &self.context().game().config.title; + self.settings_manager.save_settings(self.settings())?; + self.settings_manager + .save_global_record(game, self.global_record())?; + self.settings_manager.save_records(game, self.records())?; + Ok(()) + } + + /// Determine if current run has been visited. + pub fn current_visited(&self) -> bool { + self.current_run() + .map(|ctx| self.global_record().visited(ctx)) + .unwrap_or_default() + } + + /// Get the last action text from each record. + pub fn records_text(&self) -> impl Iterator + '_ { + self.records().iter().map(|record| { + let raw_ctx = record.history.last().unwrap(); + let action = unwrap_or_default_log!( + self.context().get_action(&self.settings().lang, raw_ctx), + "Cannot get action" + ); + if let Action::Text(action) = action { + action + } else { + unreachable!() + } + }) + } + + /// Get the current history by language and secondary language. + pub fn current_history( + &self, + ) -> impl DoubleEndedIterator)> + '_ { + self.record() + .history + .iter() + .map(|raw_ctx| self.get_actions(raw_ctx)) + } +} diff --git a/utils/ayaka-runtime/Cargo.toml b/utils/ayaka-runtime/Cargo.toml index bd88753a..9827e6d6 100644 --- a/utils/ayaka-runtime/Cargo.toml +++ b/utils/ayaka-runtime/Cargo.toml @@ -13,11 +13,10 @@ icu_locid = { version = "1.0.0", features = ["std"] } sys-locale = "0.2" serde = { workspace = true, features = ["derive"] } serde_yaml = { workspace = true } -serde_json = { workspace = true } anyhow = { workspace = true } -stream-future = "0.3" +stream-future = { workspace = true } futures-util = "0.3" -tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" } +tryiterator = { workspace = true } log = { workspace = true } trylog = { workspace = true } cfg-if = "1.0" diff --git a/utils/ayaka-runtime/src/config.rs b/utils/ayaka-runtime/src/config.rs index 7aab5534..19d7eb6b 100644 --- a/utils/ayaka-runtime/src/config.rs +++ b/utils/ayaka-runtime/src/config.rs @@ -71,6 +71,15 @@ pub struct Game { } impl Game { + /// Create a [`RawContext`] at the start of the game. + pub fn start_context(&self) -> RawContext { + RawContext { + cur_base_para: self.config.start.clone(), + cur_para: self.config.start.clone(), + ..Default::default() + } + } + fn choose_from_keys<'a, V>(&'a self, loc: &Locale, map: &'a HashMap) -> &'a Locale { loc.choose_from(map.keys()) .unwrap_or(&self.config.base_lang) diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs index 28b1cce0..a7da7d98 100644 --- a/utils/ayaka-runtime/src/context.rs +++ b/utils/ayaka-runtime/src/context.rs @@ -1,6 +1,5 @@ use crate::{ plugin::{LoadStatus, Runtime}, - settings::*, *, }; use anyhow::{anyhow, bail, Result}; @@ -16,16 +15,11 @@ use vfs::*; /// The game running context. pub struct Context { - /// The inner [`Game`] object. - pub game: Game, - /// The root path of config. - pub root_path: VfsPath, + game: Game, + root_path: VfsPath, frontend: FrontendType, runtime: Arc, - /// The inner raw context. - pub ctx: RawContext, - /// The inner record. - pub record: ActionRecord, + ctx: RawContext, switches: Vec, vars: VarMap, } @@ -47,6 +41,15 @@ pub enum OpenStatus { LoadParagraph, } +impl From for OpenStatus { + fn from(value: LoadStatus) -> Self { + match value { + LoadStatus::CreateEngine => Self::CreateRuntime, + LoadStatus::LoadPlugin(name, i, len) => Self::LoadPlugin(name, i, len), + } + } +} + const MAGIC_NUMBER_START: MagicNumber = *b"AYAPACK"; const MAGIC_NUMBER_END: MagicNumber = *b"PACKEND"; @@ -98,12 +101,7 @@ impl Context { let runtime = Runtime::load(&config.plugins.dir, &root_path, &config.plugins.modules); pin_mut!(runtime); while let Some(load_status) = runtime.next().await { - match load_status { - LoadStatus::CreateEngine => yield OpenStatus::CreateRuntime, - LoadStatus::LoadPlugin(name, i, len) => { - yield OpenStatus::LoadPlugin(name, i, len) - } - }; + yield load_status.into(); } runtime.await? }; @@ -170,27 +168,19 @@ impl Context { frontend, runtime, ctx: RawContext::default(), - record: ActionRecord::default(), switches: vec![], vars: VarMap::default(), }) } - /// Initialize the [`RawContext`] to the start of the game. - pub fn init_new(&mut self) { - self.init_context(ActionRecord { history: vec![] }) + /// Initialize the [`RawContext`] at the start of the game. + pub fn set_start_context(&mut self) { + self.set_context(self.game().start_context()) } - /// Initialize the [`ActionRecord`] with given record. - pub fn init_context(&mut self, record: ActionRecord) { - self.ctx = record.last_ctx_with_game(&self.game); - log::debug!("Context: {:?}", self.ctx); - self.record = record; - if !self.record.history.is_empty() { - // If the record is not empty, - // we need to set current context to the next one. - self.ctx.cur_act += 1; - } + /// Initialize the [`RawContext`] with given record. + pub fn set_context(&mut self, ctx: RawContext) { + self.ctx = ctx; } fn current_paragraph(&self, loc: &Locale) -> Option<&Paragraph> { @@ -214,6 +204,16 @@ impl Context { .and_then(|map| map.get(key)) } + /// The inner [`Game`] object. + pub fn game(&self) -> &Game { + &self.game + } + + /// The root path of config. + pub fn root_path(&self) -> &VfsPath { + &self.root_path + } + /// Call the part of script with this context. pub fn call(&self, text: &Text) -> String { let mut str = String::new(); @@ -251,7 +251,7 @@ impl Context { } } - fn parse_text(&self, loc: &Locale, text: &Text) -> Result { + fn parse_text(&self, loc: &Locale, text: &Text, ctx: &RawContext) -> Result { let mut action = ActionText::default(); for subtext in &text.0 { match subtext { @@ -272,7 +272,7 @@ impl Context { } } Command::Ctx(n) => { - if let Some(value) = self.ctx.locals.get(n) { + if let Some(value) = ctx.locals.get(n) { action.push_back_block(value.get_str()) } else { log::warn!("Cannot find variable {}", n) @@ -414,7 +414,7 @@ impl Context { let action = cur_text .map(|t| match t { - Line::Text(t) => self.parse_text(loc, t).map(Action::Text).ok(), + Line::Text(t) => self.parse_text(loc, t, ctx).map(Action::Text).ok(), Line::Switch { switches } => Some(Action::Switches(self.parse_switches(switches))), // The real vars will be filled in `merge_action`. Line::Custom(_) => Some(Action::Custom(self.vars.clone())), @@ -429,24 +429,6 @@ impl Context { Ok(act) } - fn push_history(&mut self) { - let ctx = &self.ctx; - let cur_text = self - .game - .find_para( - &self.game.config.base_lang, - &ctx.cur_base_para, - &ctx.cur_para, - ) - .and_then(|p| p.texts.get(ctx.cur_act)); - let is_text = cur_text - .map(|line| matches!(line, Line::Text(_))) - .unwrap_or_default(); - if is_text { - self.record.history.push(ctx.clone()); - } - } - /// Step to next line. pub fn next_run(&mut self) -> Option { let cur_text_base = loop { @@ -479,30 +461,12 @@ impl Context { let ctx = cur_text_base.cloned().map(|t| { unwrap_or_default_log!(self.process_line(t), "Parse line error"); - self.push_history(); self.ctx.clone() }); self.ctx.cur_act += 1; ctx } - /// Step back to the last run. - pub fn next_back_run(&mut self) -> Option<&RawContext> { - if self.record.history.len() <= 1 { - None - } else { - if let Some(ctx) = self.record.history.pop() { - self.ctx = ctx; - log::debug!( - "Back to para {}, act {}", - self.ctx.cur_para, - self.ctx.cur_act - ); - } - self.record.history.last() - } - } - /// Get current paragraph title. pub fn current_paragraph_title(&self, loc: &Locale) -> Option<&String> { self.current_paragraph_fallback(loc) diff --git a/utils/ayaka-runtime/src/lib.rs b/utils/ayaka-runtime/src/lib.rs index 5c9ec0ee..d2027aa1 100644 --- a/utils/ayaka-runtime/src/lib.rs +++ b/utils/ayaka-runtime/src/lib.rs @@ -13,7 +13,6 @@ mod config; mod context; mod locale; pub mod plugin; -pub mod settings; #[doc(no_inline)] pub use anyhow;