pub mod tokenizer;
pub mod wait_inserter;
pub mod line_breaker;
use crate::loader::TalkConfig;
use mlua::{Lua, Result as LuaResult, Table, Value};
use std::sync::Arc;
use tokenizer::Tokenizer;
use wait_inserter::WaitValues;
const VERSION: &str = "1.0.0";
const DESCRIPTION: &str = "Sakura Script wait insertion module for natural conversation tempo";
struct SakuraScriptState {
tokenizer: Tokenizer,
default_wait_values: WaitValues,
budoux_parser: budouy::Parser,
}
pub fn register(lua: &Lua, config: Option<&TalkConfig>) -> LuaResult<Table> {
let config = config.cloned().unwrap_or_default();
let tokenizer = Tokenizer::new(&config).map_err(|e| {
mlua::Error::external(format!("Failed to compile sakura script regex: {}", e))
})?;
let default_wait_values = WaitValues::from_config(&config);
let state = Arc::new(SakuraScriptState {
tokenizer,
default_wait_values,
budoux_parser: budouy::model::load_default_japanese_parser(),
});
let module = lua.create_table()?;
module.set("_VERSION", VERSION)?;
module.set("_DESCRIPTION", DESCRIPTION)?;
let state_clone = Arc::clone(&state);
let talk_to_script =
lua.create_function(move |_lua, (actor, talk): (Value, Option<String>)| {
talk_to_script_impl(&state_clone, actor, talk)
})?;
module.set("talk_to_script", talk_to_script)?;
let state_clone = Arc::clone(&state);
let break_lines =
lua.create_function(move |_lua, (text, widths): (Option<String>, Value)| {
break_lines_lua_impl(&state_clone, text, widths)
})?;
module.set("break_lines", break_lines)?;
Ok(module)
}
fn talk_to_script_impl(
state: &SakuraScriptState,
actor: Value,
talk: Option<String>,
) -> LuaResult<String> {
let talk = match talk {
Some(s) if !s.is_empty() => s,
_ => return Ok(String::new()),
};
let wait_values = resolve_wait_values(&actor, &state.default_wait_values)?;
let tokens = state.tokenizer.tokenize(&talk);
let result = wait_inserter::insert_waits(&tokens, &wait_values);
let result = apply_budoux_if_configured(&actor, state, result)?;
Ok(result)
}
fn apply_budoux_if_configured(
actor: &Value,
state: &SakuraScriptState,
text: String,
) -> LuaResult<String> {
let actor_table = match actor {
Value::Table(t) => t,
_ => return Ok(text),
};
let widths: Vec<usize> = match actor_table.get::<Value>("budoux")? {
Value::Table(t) => table_to_widths(&t)?,
_ => return Ok(text),
};
if widths.is_empty() {
return Ok(text);
}
Ok(line_breaker::break_lines_impl(
&text,
&widths,
state.tokenizer.tag_regex(),
&state.budoux_parser,
))
}
fn table_to_widths(t: &Table) -> LuaResult<Vec<usize>> {
let mut v = Vec::new();
for i in 1..=t.raw_len() {
let w: i64 = t.get(i)?;
v.push(w as usize);
}
Ok(v)
}
fn resolve_wait_values(actor: &Value, defaults: &WaitValues) -> LuaResult<WaitValues> {
let actor_table = match actor {
Value::Table(t) => t,
_ => return Ok(defaults.clone()),
};
let get_wait =
|key: &str, default: i64| -> i64 { actor_table.get::<i64>(key).ok().unwrap_or(default) };
Ok(WaitValues {
normal: get_wait("script_wait_normal", defaults.normal),
period: get_wait("script_wait_period", defaults.period),
comma: get_wait("script_wait_comma", defaults.comma),
strong: get_wait("script_wait_strong", defaults.strong),
leader: get_wait("script_wait_leader", defaults.leader),
})
}
fn break_lines_lua_impl(
state: &SakuraScriptState,
text: Option<String>,
widths: Value,
) -> LuaResult<String> {
let text = match text {
Some(s) if !s.is_empty() => s,
_ => return Ok(String::new()),
};
let widths_vec: Vec<usize> = match widths {
Value::Table(t) => table_to_widths(&t)?,
_ => return Ok(text),
};
if widths_vec.is_empty() {
return Ok(text);
}
let result = line_breaker::break_lines_impl(
&text,
&widths_vec,
state.tokenizer.tag_regex(),
&state.budoux_parser,
);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_creates_module_table() {
let lua = Lua::new();
let config = TalkConfig::default();
let module = register(&lua, Some(&config)).unwrap();
assert!(module.contains_key("_VERSION").unwrap());
assert!(module.contains_key("_DESCRIPTION").unwrap());
assert!(module.contains_key("talk_to_script").unwrap());
assert!(module.contains_key("break_lines").unwrap());
}
#[test]
fn test_version_and_description() {
let lua = Lua::new();
let config = TalkConfig::default();
let module = register(&lua, Some(&config)).unwrap();
let version: String = module.get("_VERSION").unwrap();
assert_eq!(version, "1.0.0");
let desc: String = module.get("_DESCRIPTION").unwrap();
assert!(!desc.is_empty());
}
}