diff --git a/src/plugins/video/mod.rs b/src/plugins/video/mod.rs index d03b386..2497fc2 100644 --- a/src/plugins/video/mod.rs +++ b/src/plugins/video/mod.rs @@ -147,9 +147,25 @@ impl Plugin for VideoPlugin { match command { PlayerCommand::Play => { processor.play()?; + // 恢复播放时重新获取防息屏锁 + if let Some(ctx) = &self.ctx { + let _ = ctx.tx.send(Envelope { + from: self.id(), + to: Destination::Plugin("screen"), + message: Message::ScreenLockRequest(true), + }); + } } PlayerCommand::Pause => { processor.pause(); + // 暂停时释放防息屏锁 + if let Some(ctx) = &self.ctx { + let _ = ctx.tx.send(Envelope { + from: self.id(), + to: Destination::Plugin("screen"), + message: Message::ScreenLockRequest(false), + }); + } } PlayerCommand::Next => { processor.next_video()?; diff --git a/src/plugins/video/state_machine.rs b/src/plugins/video/state_machine.rs index 29547ff..5729a3e 100644 --- a/src/plugins/video/state_machine.rs +++ b/src/plugins/video/state_machine.rs @@ -233,11 +233,42 @@ impl StateMachine { return Ok(next_state.clone()); } + // FreeMode 状态没有配置 next_state/next_states 时,按权重随机选择一个 FreeMode 状态 match state.mode { - StateMode::FreeMode | StateMode::InteractiveMode => Ok(self.current_state.clone()), + StateMode::FreeMode => self.select_random_free_state(), + StateMode::InteractiveMode => Ok(self.current_state.clone()), } } + /// 按权重随机选择一个 FreeMode 状态(旧版行为回补) + fn select_random_free_state(&self) -> Result { + let free_states: Vec<(&String, &StateConfig)> = self + .config + .states + .iter() + .filter(|(_, state)| matches!(state.mode, StateMode::FreeMode)) + .collect(); + + if free_states.is_empty() { + anyhow::bail!("没有可用的 FreeMode 状态"); + } + + let total_weight: f32 = free_states.iter().map(|(_, state)| state.weight).sum(); + if total_weight <= 0.0 { + return Ok(free_states[0].0.clone()); + } + + let mut cursor = rand::thread_rng().gen_range(0.0..total_weight); + for (name, state) in &free_states { + cursor -= state.weight; + if cursor <= 0.0 { + return Ok((*name).clone()); + } + } + + Ok(free_states.last().unwrap().0.clone()) + } + fn select_weighted_next_state(&self, entries: &[NextStateEntry]) -> Option { let valid_entries: Vec<&NextStateEntry> = entries.iter().filter(|entry| entry.weight > 0.0).collect(); @@ -443,6 +474,89 @@ mod tests { assert_eq!(machine.current_state, "weighted"); } + #[test] + fn free_mode_random_walk_when_no_next_state_configured() { + let mut machine = StateMachine::new(config_with_states([ + StateConfig { + name: "idle".to_string(), + mode: StateMode::FreeMode, + sequence: vec![step("idle")], + next_state: None, + next_states: None, + transitions: vec![], + weight: 2.0, + defer_triggers: false, + ignore_triggers: false, + }, + StateConfig { + name: "walk".to_string(), + mode: StateMode::FreeMode, + sequence: vec![step("walk")], + next_state: None, + next_states: None, + transitions: vec![], + weight: 3.0, + defer_triggers: false, + ignore_triggers: false, + }, + StateConfig { + name: "interactive".to_string(), + mode: StateMode::InteractiveMode, + sequence: vec![step("interactive")], + next_state: None, + next_states: None, + transitions: vec![], + weight: 1.0, + defer_triggers: false, + ignore_triggers: false, + }, + ])); + machine.start().expect("state machine should start"); + + let mut visited_states = std::collections::HashSet::new(); + for _ in 0..20 { + machine + .on_video_completed() + .expect("should transition to random free state"); + visited_states.insert(machine.current_state.clone()); + + // 验证只会跳转到 FreeMode 状态 + assert!( + machine.current_state == "idle" || machine.current_state == "walk", + "应该只跳转到 FreeMode 状态,但跳转到了: {}", + machine.current_state + ); + } + + // 验证随机性:20 次跳转应该至少访问过两个状态 + assert!( + visited_states.len() >= 2, + "随机游走应该访问多个状态,但只访问了: {:?}", + visited_states + ); + } + + #[test] + fn interactive_mode_stays_in_same_state_when_no_next_state_configured() { + let mut machine = StateMachine::new(config_with_states([StateConfig { + name: "interactive".to_string(), + mode: StateMode::InteractiveMode, + sequence: vec![step("interactive")], + next_state: None, + next_states: None, + transitions: vec![], + weight: 1.0, + defer_triggers: false, + ignore_triggers: false, + }])); + machine.start().expect("state machine should start"); + + assert!(!machine + .on_video_completed() + .expect("interactive mode should stay in same state")); + assert_eq!(machine.current_state, "interactive"); + } + fn config_with_states(states: [StateConfig; N]) -> StateMachineConfig { let states = Vec::from(states) .into_iter()