feat: free mode random walk + pause wake lock release
State machine (张明远): - Add select_random_free_state() for FreeMode states without next_state/next_states config - Weighted random selection across all FreeMode states - Matches old hologram_player_rust behavior Video plugin (赵雨薇): - Send ScreenLockRequest(false) to screen plugin on Pause - Send ScreenLockRequest(true) to screen plugin on Play/Resume - Closes the pause-wake-lock gap vs old version cargo check: 0 warnings, cargo test: 22/22 passed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()?;
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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<const N: usize>(states: [StateConfig; N]) -> StateMachineConfig {
|
||||
let states = Vec::from(states)
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user