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:
showen
2026-03-12 12:56:45 +08:00
parent b3cf12359e
commit cc4d6935d9
2 changed files with 131 additions and 1 deletions

View File

@@ -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()?;

View File

@@ -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()