diff --git a/decisions/questions-scope.md b/decisions/questions-scope.md index db92719b7..44d68448a 100644 --- a/decisions/questions-scope.md +++ b/decisions/questions-scope.md @@ -92,4 +92,12 @@ Game concept, prototype boundaries, production pipeline, and feature decisions. --- -*14 questions (1 resolved, 3 partially resolved, 10 open). Last updated: 2026-03-05 (Q-011 resolved, Q-034 and Q-037 partially resolved — Where's the Fun? Workshop)* +### Q-058: Runtime behavior text serving system +- **Status:** Open +- **Question:** How should NPC observable behaviors be served to the client at runtime? `NpcBlueprint.observable_behaviors` exists as generator output but no runtime system reads it or sends behavior text to the client. The voice pipeline (D-138) needs an integration point: voice cache lookup replaces base text with re-voiced text before delivery. Needs: which system selects the current behavior, how it's delivered in `ObserverSnapshot`, and how tell behaviors (always passthrough) are distinguished from voiceable behaviors. +- **Assigned to:** Tyre, SI +- **Source:** Voice pipeline Spike 2 Phase 3 + +--- + +*15 questions (1 resolved, 3 partially resolved, 11 open). Last updated: 2026-03-07 (Q-058 added — voice pipeline Phase 3 dependency)* diff --git a/decisions/questions.md b/decisions/questions.md index 5aba5fcb3..77e113cdc 100644 --- a/decisions/questions.md +++ b/decisions/questions.md @@ -9,7 +9,7 @@ Tracked questions awaiting discussion or resolution. Split by domain, mirroring | [questions-architecture.md](questions-architecture.md) | Technical foundation | Q-001, Q-006, Q-009, Q-018, Q-019, Q-020, Q-021, Q-022, Q-023, Q-029, Q-030, Q-046 | | [questions-perception.md](questions-perception.md) | Player observation | Q-003, Q-014, Q-016, Q-024, Q-025, Q-026, Q-051, Q-053, Q-054 | | [questions-content.md](questions-content.md) | Narrative, NPCs, setting | Q-010, Q-012, Q-013, Q-015, Q-017, Q-028, Q-031, Q-033, Q-040, Q-041, Q-042, Q-043, Q-044, Q-045, Q-047, Q-048, Q-049, Q-050, Q-052, Q-056 | -| [questions-scope.md](questions-scope.md) | Game concept, prototype | Q-002, Q-004, Q-005, Q-007, Q-008, Q-011, Q-027, Q-032, Q-034, Q-035, Q-036, Q-037, Q-038, Q-039 | +| [questions-scope.md](questions-scope.md) | Game concept, prototype | Q-002, Q-004, Q-005, Q-007, Q-008, Q-011, Q-027, Q-032, Q-034, Q-035, Q-036, Q-037, Q-038, Q-039, Q-058 | ## Status Summary diff --git a/server/src/voice/lookup.rs b/server/src/voice/lookup.rs new file mode 100644 index 000000000..e0698063e --- /dev/null +++ b/server/src/voice/lookup.rs @@ -0,0 +1,122 @@ +//! 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>, + zone_id: u32, + culture_id: &str, + npc_stable_id: u64, + content_type: ContentType, + content_index: u16, + tell_state: Option, + 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> { + 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."); + } +} diff --git a/server/src/voice/mod.rs b/server/src/voice/mod.rs index 1264b4e85..d11b695bb 100644 --- a/server/src/voice/mod.rs +++ b/server/src/voice/mod.rs @@ -13,6 +13,7 @@ //! - `hardware` — hardware detection + dynamic sr-voice instance management pub mod cache; +pub mod lookup; pub mod prompt_builder; pub mod queue; pub mod worker;