Add lookup.rs with voiced_behavior() — ready to wire into a behavior-serving system once one exists (Q-058). Tell behaviors always passthrough (never re-voiced). Cache miss returns base text (graceful degradation). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
3.7 KiB
Rust
123 lines
3.7 KiB
Rust
//! Voice cache lookup for behavior text (D-138, Phase 3 stub).
|
|
//!
|
|
//! Provides the integration point between the voice cache and any system
|
|
//! that serves NPC text to the client. Not yet wired into a runtime system —
|
|
//! `observable_behaviors` on `NpcBlueprint` is generator output only.
|
|
//!
|
|
//! Wire `voiced_behavior()` into the behavior-serving path once it exists.
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::npc::tell_state::TellCategory;
|
|
use crate::voice::cache::{CacheKey, VoiceCacheStore};
|
|
use crate::voice::prompt_builder::ContentType;
|
|
|
|
/// Look up a voiced behavior from cache, falling back to base text.
|
|
///
|
|
/// Tell behaviors (from `NpcBlueprint.tell_behaviors`) are NEVER re-voiced —
|
|
/// they are always base text passthrough. Only pass `is_tell_behavior: false`
|
|
/// for `observable_behaviors` content.
|
|
pub fn voiced_behavior(
|
|
cache: &Arc<Mutex<VoiceCacheStore>>,
|
|
zone_id: u32,
|
|
culture_id: &str,
|
|
npc_stable_id: u64,
|
|
content_type: ContentType,
|
|
content_index: u16,
|
|
tell_state: Option<TellCategory>,
|
|
base_text: &str,
|
|
is_tell_behavior: bool,
|
|
) -> String {
|
|
// Tell behaviors are always passthrough — never re-voiced.
|
|
if is_tell_behavior {
|
|
return base_text.to_string();
|
|
}
|
|
|
|
let key = CacheKey {
|
|
culture_id: culture_id.to_string(),
|
|
npc_stable_id,
|
|
content_type,
|
|
content_index,
|
|
tell_state,
|
|
};
|
|
|
|
if let Ok(mut store) = cache.lock() {
|
|
if let Some(voiced) = store.lookup(zone_id, &key) {
|
|
return voiced;
|
|
}
|
|
}
|
|
|
|
// Cache miss — serve base text (graceful degradation).
|
|
base_text.to_string()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::PathBuf;
|
|
|
|
fn test_cache() -> Arc<Mutex<VoiceCacheStore>> {
|
|
let dir = std::env::temp_dir().join("sr-voice-lookup-test");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
Arc::new(Mutex::new(VoiceCacheStore::new(
|
|
dir,
|
|
99,
|
|
"v1".into(),
|
|
"i1".into(),
|
|
)))
|
|
}
|
|
|
|
#[test]
|
|
fn cache_hit_returns_voiced_text() {
|
|
let cache = test_cache();
|
|
let key = CacheKey {
|
|
culture_id: "krenn".into(),
|
|
npc_stable_id: 42,
|
|
content_type: ContentType::Behavior,
|
|
content_index: 0,
|
|
tell_state: None,
|
|
};
|
|
cache.lock().unwrap().store(100, key, "Voiced line.".into());
|
|
|
|
let result = voiced_behavior(
|
|
&cache, 100, "krenn", 42, ContentType::Behavior, 0, None,
|
|
"Base line.", false,
|
|
);
|
|
assert_eq!(result, "Voiced line.");
|
|
}
|
|
|
|
#[test]
|
|
fn cache_miss_returns_base_text() {
|
|
let cache = test_cache();
|
|
let result = voiced_behavior(
|
|
&cache, 100, "krenn", 42, ContentType::Behavior, 0, None,
|
|
"Base line.", false,
|
|
);
|
|
assert_eq!(result, "Base line.");
|
|
}
|
|
|
|
#[test]
|
|
fn tell_behavior_always_passthrough() {
|
|
let cache = test_cache();
|
|
// Even if cache has a voiced version, tell behaviors return base text
|
|
let key = CacheKey {
|
|
culture_id: "krenn".into(),
|
|
npc_stable_id: 42,
|
|
content_type: ContentType::Behavior,
|
|
content_index: 0,
|
|
tell_state: Some(TellCategory::Angry),
|
|
};
|
|
cache.lock().unwrap().store(100, key, "Voiced tell.".into());
|
|
|
|
let result = voiced_behavior(
|
|
&cache, 100, "krenn", 42, ContentType::Behavior, 0,
|
|
Some(TellCategory::Angry), "Base tell.", true,
|
|
);
|
|
assert_eq!(result, "Base tell.");
|
|
}
|
|
}
|