diff --git a/assets/locales/app.yaml b/assets/locales/app.yaml index caf14a9..3f8bf50 100644 --- a/assets/locales/app.yaml +++ b/assets/locales/app.yaml @@ -34,6 +34,8 @@ stats_window: label: none: en: None + cancel: + en: Cancel clear: en: Clear select: diff --git a/raphael-cli/src/commands/solve.rs b/raphael-cli/src/commands/solve.rs index dc00376..f80ddd2 100644 --- a/raphael-cli/src/commands/solve.rs +++ b/raphael-cli/src/commands/solve.rs @@ -1,7 +1,7 @@ use clap::Args; use game_data::{get_game_settings, CrafterStats, MEALS, POTIONS, RECIPES}; use simulator::SimulationState; -use solvers::MacroSolver; +use solvers::{AtomicFlag, MacroSolver}; #[derive(Args, Debug)] pub struct SolveArgs { @@ -258,6 +258,7 @@ pub fn execute(args: &SolveArgs) { args.unsound, Box::new(|_| {}), Box::new(|_| {}), + AtomicFlag::new(), ); let actions = solver.solve(state).expect("Failed to solve"); diff --git a/solvers/examples/macro_solver_example.rs b/solvers/examples/macro_solver_example.rs index 6e94afb..24c142f 100644 --- a/solvers/examples/macro_solver_example.rs +++ b/solvers/examples/macro_solver_example.rs @@ -1,5 +1,5 @@ use simulator::{Action, ActionMask, Settings, SimulationState}; -use solvers::MacroSolver; +use solvers::{AtomicFlag, MacroSolver}; fn main() { #[cfg(feature = "env_logger")] @@ -32,9 +32,16 @@ fn main() { }; let state = SimulationState::new(&settings); - let actions = MacroSolver::new(settings, false, false, Box::new(|_| {}), Box::new(|_| {})) - .solve(state) - .unwrap(); + let actions = MacroSolver::new( + settings, + false, + false, + Box::new(|_| {}), + Box::new(|_| {}), + AtomicFlag::new(), + ) + .solve(state) + .unwrap(); let quality = SimulationState::from_macro(&settings, &actions) .unwrap() diff --git a/solvers/src/lib.rs b/solvers/src/lib.rs index 6d23396..455b2f2 100644 --- a/solvers/src/lib.rs +++ b/solvers/src/lib.rs @@ -1,6 +1,5 @@ mod actions; mod branch_pruning; -mod utils; mod finish_solver; use finish_solver::FinishSolver; @@ -14,8 +13,11 @@ use step_lower_bound_solver::StepLowerBoundSolver; mod macro_solver; pub use macro_solver::MacroSolver; +mod utils; +pub use utils::AtomicFlag; + pub mod test_utils { - use crate::MacroSolver; + use crate::{utils::AtomicFlag, MacroSolver}; use simulator::*; pub fn solve( @@ -29,6 +31,7 @@ pub mod test_utils { unsound_branch_pruning, Box::new(|_| {}), Box::new(|_| {}), + AtomicFlag::new(), ) .solve(SimulationState::new(settings)) } diff --git a/solvers/src/macro_solver/fast_lower_bound.rs b/solvers/src/macro_solver/fast_lower_bound.rs index 5f0dad7..51787ac 100644 --- a/solvers/src/macro_solver/fast_lower_bound.rs +++ b/solvers/src/macro_solver/fast_lower_bound.rs @@ -20,7 +20,7 @@ pub fn fast_lower_bound( settings: &Settings, finish_solver: &mut FinishSolver, upper_bound_solver: &mut QualityUpperBoundSolver, -) -> u16 { +) -> Option { let _timer = NamedTimer::new("Fast lower bound"); let allowed_actions = settings.allowed_actions.intersection(SEARCH_ACTIONS); @@ -29,7 +29,7 @@ pub fn fast_lower_bound( let mut quality_lower_bound = 0; - search_queue.push(upper_bound_solver.quality_upper_bound(state), state); + search_queue.push(upper_bound_solver.quality_upper_bound(state)?, state); while let Some((score, state)) = search_queue.pop() { if score <= quality_lower_bound { @@ -48,7 +48,7 @@ pub fn fast_lower_bound( if action == Action::ByregotsBlessing { continue; } - let quality_upper_bound = upper_bound_solver.quality_upper_bound(state); + let quality_upper_bound = upper_bound_solver.quality_upper_bound(state)?; if quality_upper_bound <= quality_lower_bound { continue; } @@ -62,7 +62,7 @@ pub fn fast_lower_bound( } log::debug!("Fast quality lower bound: {}", quality_lower_bound); - std::cmp::min(settings.max_quality, quality_lower_bound) + Some(std::cmp::min(settings.max_quality, quality_lower_bound)) } fn should_use_action(action: Action, state: &SimulationState, allowed_actions: ActionMask) -> bool { diff --git a/solvers/src/macro_solver/solver.rs b/solvers/src/macro_solver/solver.rs index ae1f2b6..2cdfc0e 100644 --- a/solvers/src/macro_solver/solver.rs +++ b/solvers/src/macro_solver/solver.rs @@ -1,11 +1,14 @@ use simulator::{Action, ActionMask, Condition, Settings, SimulationState}; use super::search_queue::SearchScore; -use crate::actions::{DURABILITY_ACTIONS, PROGRESS_ACTIONS, QUALITY_ACTIONS}; use crate::branch_pruning::{is_progress_only_state, strip_quality_effects}; use crate::macro_solver::fast_lower_bound::fast_lower_bound; use crate::macro_solver::search_queue::SearchQueue; use crate::utils::NamedTimer; +use crate::{ + actions::{DURABILITY_ACTIONS, PROGRESS_ACTIONS, QUALITY_ACTIONS}, + utils::AtomicFlag, +}; use crate::{FinishSolver, QualityUpperBoundSolver, StepLowerBoundSolver}; use std::vec::Vec; @@ -36,6 +39,7 @@ pub struct MacroSolver<'a> { step_lower_bound_solver: StepLowerBoundSolver, solution_callback: Box>, progress_callback: Box>, + interrupt_signal: AtomicFlag, } impl<'a> MacroSolver<'a> { @@ -45,6 +49,7 @@ impl<'a> MacroSolver<'a> { unsound_branch_pruning: bool, solution_callback: Box>, progress_callback: Box>, + interrupt_signal: AtomicFlag, ) -> MacroSolver<'a> { MacroSolver { settings, @@ -55,14 +60,17 @@ impl<'a> MacroSolver<'a> { settings, backload_progress, unsound_branch_pruning, + interrupt_signal.clone(), ), step_lower_bound_solver: StepLowerBoundSolver::new( settings, backload_progress, unsound_branch_pruning, + interrupt_signal.clone(), ), solution_callback, progress_callback, + interrupt_signal, } } @@ -82,9 +90,9 @@ impl<'a> MacroSolver<'a> { fn do_solve(&mut self, state: SimulationState) -> Option> { let mut search_queue = { let _timer = NamedTimer::new("Initial upper bound"); - let quality_upper_bound = self.quality_upper_bound_solver.quality_upper_bound(state); + let quality_upper_bound = self.quality_upper_bound_solver.quality_upper_bound(state)?; let step_lower_bound = if quality_upper_bound >= self.settings.max_quality { - self.step_lower_bound_solver.step_lower_bound(state) + self.step_lower_bound_solver.step_lower_bound(state)? } else { 1 // quality dominates the search score, so no need to query the step solver }; @@ -94,7 +102,7 @@ impl<'a> MacroSolver<'a> { &self.settings, &mut self.finish_solver, &mut self.quality_upper_bound_solver, - ); + )?; let minimum_score = SearchScore::new(quality_lower_bound, u8::MAX, u8::MAX); SearchQueue::new(state, initial_score, minimum_score) }; @@ -103,6 +111,10 @@ impl<'a> MacroSolver<'a> { let mut popped = 0; while let Some((state, score, backtrack_id)) = search_queue.pop() { + if self.interrupt_signal.is_set() { + return None; + } + popped += 1; if popped % (1 << 14) == 0 { (self.progress_callback)(popped); @@ -136,14 +148,14 @@ impl<'a> MacroSolver<'a> { } else { std::cmp::min( score.quality, - self.quality_upper_bound_solver.quality_upper_bound(state), + self.quality_upper_bound_solver.quality_upper_bound(state)?, ) }; let step_lb_hint = score.steps.saturating_sub(current_steps + 1); let step_lower_bound = if quality_upper_bound >= self.settings.max_quality { self.step_lower_bound_solver - .step_lower_bound_with_hint(state, step_lb_hint) + .step_lower_bound_with_hint(state, step_lb_hint)? .saturating_add(current_steps + 1) } else { current_steps + 1 diff --git a/solvers/src/quality_upper_bound_solver/solver.rs b/solvers/src/quality_upper_bound_solver/solver.rs index e20e79f..db29d9e 100644 --- a/solvers/src/quality_upper_bound_solver/solver.rs +++ b/solvers/src/quality_upper_bound_solver/solver.rs @@ -2,7 +2,7 @@ use std::i16; use crate::{ actions::{PROGRESS_ACTIONS, QUALITY_ACTIONS}, - utils::{ParetoFrontBuilder, ParetoFrontId, ParetoValue}, + utils::{AtomicFlag, ParetoFrontBuilder, ParetoFrontId, ParetoValue}, }; use simulator::*; @@ -31,13 +31,19 @@ pub struct QualityUpperBoundSolver { solver_settings: SolverSettings, solved_states: HashMap, pareto_front_builder: ParetoFrontBuilder, + interrupt_signal: AtomicFlag, // pre-computed branch pruning values waste_not_1_min_cp: i16, waste_not_2_min_cp: i16, } impl QualityUpperBoundSolver { - pub fn new(settings: Settings, backload_progress: bool, unsound_branch_pruning: bool) -> Self { + pub fn new( + settings: Settings, + backload_progress: bool, + unsound_branch_pruning: bool, + interrupt_signal: AtomicFlag, + ) -> Self { log::trace!( "ReducedState (QualityUpperBoundSolver) - size: {}, align: {}", std::mem::size_of::(), @@ -77,6 +83,7 @@ impl QualityUpperBoundSolver { settings.max_progress, settings.max_quality, ), + interrupt_signal, waste_not_1_min_cp: waste_not_min_cp(56, 4, durability_cost), waste_not_2_min_cp: waste_not_min_cp(98, 8, durability_cost), } @@ -84,7 +91,11 @@ impl QualityUpperBoundSolver { /// Returns an upper-bound on the maximum Quality achievable from this state while also maxing out Progress. /// There is no guarantee on the tightness of the upper-bound. - pub fn quality_upper_bound(&mut self, state: SimulationState) -> u16 { + pub fn quality_upper_bound(&mut self, state: SimulationState) -> Option { + if self.interrupt_signal.is_set() { + return None; + } + let current_quality = state.quality; let missing_progress = self .simulator_settings @@ -108,10 +119,10 @@ impl QualityUpperBoundSolver { match pareto_front.last() { Some(element) => { if element.first < missing_progress { - return 0; + return Some(0); } } - None => return 0, + None => return Some(0), } let index = match pareto_front.binary_search_by_key(&missing_progress, |value| value.first) @@ -120,21 +131,25 @@ impl QualityUpperBoundSolver { Err(i) => i, }; - std::cmp::min( + Some(std::cmp::min( self.simulator_settings.max_quality, pareto_front[index].second.saturating_add(current_quality), - ) + )) } - fn solve_state(&mut self, state: ReducedState) { + fn solve_state(&mut self, state: ReducedState) -> Option<()> { + if self.interrupt_signal.is_set() { + return None; + } + if state.data.combo() == Combo::None { - self.solve_normal_state(state); + self.solve_normal_state(state) } else { self.solve_combo_state(state) } } - fn solve_normal_state(&mut self, state: ReducedState) { + fn solve_normal_state(&mut self, state: ReducedState) -> Option<()> { self.pareto_front_builder.push_empty(); let search_actions = if state.data.progress_only() { PROGRESS_SEARCH_ACTIONS.intersection(self.simulator_settings.allowed_actions) @@ -145,7 +160,7 @@ impl QualityUpperBoundSolver { if !self.should_use_action(state, action) { continue; } - self.build_child_front(state, action); + self.build_child_front(state, action)?; if self.pareto_front_builder.is_max() { // stop early if both Progress and Quality are maxed out // this optimization would work even better with better action ordering @@ -155,38 +170,46 @@ impl QualityUpperBoundSolver { } let id = self.pareto_front_builder.save().unwrap(); self.solved_states.insert(state, id); + + Some(()) } - fn solve_combo_state(&mut self, state: ReducedState) { + fn solve_combo_state(&mut self, state: ReducedState) -> Option<()> { match self.solved_states.get(&state.drop_combo()) { Some(id) => self.pareto_front_builder.push_from_id(*id), - None => self.solve_normal_state(state.drop_combo()), + None => self.solve_normal_state(state.drop_combo())?, } match state.data.combo() { Combo::None => unreachable!(), Combo::SynthesisBegin => { - self.build_child_front(state, Action::MuscleMemory); - self.build_child_front(state, Action::Reflect); - self.build_child_front(state, Action::TrainedEye); + self.build_child_front(state, Action::MuscleMemory)?; + self.build_child_front(state, Action::Reflect)?; + self.build_child_front(state, Action::TrainedEye)?; } Combo::BasicTouch => { - self.build_child_front(state, Action::RefinedTouch); - self.build_child_front(state, Action::StandardTouch); + self.build_child_front(state, Action::RefinedTouch)?; + self.build_child_front(state, Action::StandardTouch)?; } Combo::StandardTouch => { - self.build_child_front(state, Action::AdvancedTouch); + self.build_child_front(state, Action::AdvancedTouch)?; } } + + Some(()) } - fn build_child_front(&mut self, state: ReducedState, action: Action) { + fn build_child_front(&mut self, state: ReducedState, action: Action) -> Option<()> { + if self.interrupt_signal.is_set() { + return None; + } + if let Ok((new_state, action_progress, action_quality)) = state.use_action(action, &self.simulator_settings, &self.solver_settings) { if new_state.data.cp() >= self.solver_settings.durability_cost { match self.solved_states.get(&new_state) { Some(id) => self.pareto_front_builder.push_from_id(*id), - None => self.solve_state(new_state), + None => self.solve_state(new_state)?, } self.pareto_front_builder.map(move |value| { value.first += action_progress; @@ -203,6 +226,8 @@ impl QualityUpperBoundSolver { self.pareto_front_builder.merge(); } } + + Some(()) } fn should_use_action(&self, state: ReducedState, action: Action) -> bool { @@ -245,7 +270,9 @@ mod tests { fn solve(settings: Settings, actions: &[Action]) -> u16 { let state = SimulationState::from_macro(&settings, actions).unwrap(); - QualityUpperBoundSolver::new(settings, false, false).quality_upper_bound(state) + QualityUpperBoundSolver::new(settings, false, false, AtomicFlag::new()) + .quality_upper_bound(state) + .unwrap() } #[test] @@ -721,15 +748,15 @@ mod tests { /// Test that the upper-bound solver is monotonic, /// i.e. the quality UB of a state is never less than the quality UB of any of its children. fn monotonic_fuzz_check(settings: Settings) { - let mut solver = QualityUpperBoundSolver::new(settings, false, false); + let mut solver = QualityUpperBoundSolver::new(settings, false, false, AtomicFlag::new()); for _ in 0..10000 { let state = random_state(&settings); - let state_upper_bound = solver.quality_upper_bound(state); + let state_upper_bound = solver.quality_upper_bound(state).unwrap(); for action in settings.allowed_actions.actions_iter() { let child_upper_bound = match state.use_action(action, Condition::Normal, &settings) { Ok(child) => match child.is_final(&settings) { - false => solver.quality_upper_bound(child), + false => solver.quality_upper_bound(child).unwrap(), true if child.progress >= settings.max_progress => { std::cmp::min(settings.max_quality, child.quality) } diff --git a/solvers/src/step_lower_bound_solver/solver.rs b/solvers/src/step_lower_bound_solver/solver.rs index a68654d..9be4037 100644 --- a/solvers/src/step_lower_bound_solver/solver.rs +++ b/solvers/src/step_lower_bound_solver/solver.rs @@ -1,7 +1,7 @@ use crate::{ actions::{DURABILITY_ACTIONS, PROGRESS_ACTIONS, QUALITY_ACTIONS}, branch_pruning::is_progress_only_state, - utils::{ParetoFrontBuilder, ParetoFrontId, ParetoValue}, + utils::{AtomicFlag, ParetoFrontBuilder, ParetoFrontId, ParetoValue}, }; use simulator::*; @@ -24,6 +24,7 @@ pub struct StepLowerBoundSolver { bonus_durability_restore: i8, solved_states: HashMap, pareto_front_builder: ParetoFrontBuilder, + interrupt_signal: AtomicFlag, } impl StepLowerBoundSolver { @@ -31,6 +32,7 @@ impl StepLowerBoundSolver { mut settings: Settings, backload_progress: bool, unsound_branch_pruning: bool, + interrupt_signal: AtomicFlag, ) -> Self { log::trace!( "ReducedState (StepLowerBoundSolver) - size: {}, align: {}", @@ -57,29 +59,38 @@ impl StepLowerBoundSolver { settings.max_progress, settings.max_quality, ), + interrupt_signal, } } /// Returns a lower-bound on the additional steps required to max out both Progress and Quality from this state. - pub fn step_lower_bound(&mut self, state: SimulationState) -> u8 { + pub fn step_lower_bound(&mut self, state: SimulationState) -> Option { self.step_lower_bound_with_hint(state, 1) } - pub fn step_lower_bound_with_hint(&mut self, state: SimulationState, mut hint: u8) -> u8 { + pub fn step_lower_bound_with_hint( + &mut self, + state: SimulationState, + mut hint: u8, + ) -> Option { if self.backload_progress && state.progress != 0 && state.quality < self.settings.max_quality { - return u8::MAX; + return Some(u8::MAX); } hint = std::cmp::max(1, hint); - while self.quality_upper_bound(state, hint) < self.settings.max_quality { + while self.quality_upper_bound(state, hint)? < self.settings.max_quality { hint += 1; } - hint + Some(hint) } - fn quality_upper_bound(&mut self, state: SimulationState, step_budget: u8) -> u16 { + fn quality_upper_bound(&mut self, state: SimulationState, step_budget: u8) -> Option { + if self.interrupt_signal.is_set() { + return None; + } + let current_quality = state.quality; let missing_progress = self.settings.max_progress.saturating_sub(state.progress); @@ -99,10 +110,10 @@ impl StepLowerBoundSolver { match pareto_front.last() { Some(element) => { if element.first < missing_progress { - return 0; + return Some(0); } } - None => return 0, + None => return Some(0), } let index = match pareto_front.binary_search_by_key(&missing_progress, |value| value.first) @@ -110,28 +121,32 @@ impl StepLowerBoundSolver { Ok(i) => i, Err(i) => i, }; - std::cmp::min( + Some(std::cmp::min( self.settings.max_quality.saturating_mul(2), pareto_front[index].second.saturating_add(current_quality), - ) + )) } - fn solve_state(&mut self, reduced_state: ReducedState) { + fn solve_state(&mut self, reduced_state: ReducedState) -> Option<()> { + if self.interrupt_signal.is_set() { + return None; + } + if reduced_state.combo == Combo::None { - self.solve_normal_state(reduced_state); + self.solve_normal_state(reduced_state) } else { self.solve_combo_state(reduced_state) } } - fn solve_normal_state(&mut self, reduced_state: ReducedState) { + fn solve_normal_state(&mut self, reduced_state: ReducedState) -> Option<()> { self.pareto_front_builder.push_empty(); let search_actions = match reduced_state.progress_only { false => FULL_SEARCH_ACTIONS, true => PROGRESS_SEARCH_ACTIONS, }; for action in search_actions.actions_iter() { - self.build_child_front(reduced_state, action); + self.build_child_front(reduced_state, action)?; if self.pareto_front_builder.is_max() { // stop early if both Progress and Quality are maxed out // this optimization would work even better with better action ordering @@ -141,30 +156,38 @@ impl StepLowerBoundSolver { } let id = self.pareto_front_builder.save().unwrap(); self.solved_states.insert(reduced_state, id); + + Some(()) } - fn solve_combo_state(&mut self, reduced_state: ReducedState) { + fn solve_combo_state(&mut self, reduced_state: ReducedState) -> Option<()> { match self.solved_states.get(&reduced_state.to_non_combo()) { Some(id) => self.pareto_front_builder.push_from_id(*id), - None => self.solve_normal_state(reduced_state.to_non_combo()), + None => self.solve_normal_state(reduced_state.to_non_combo())?, } match reduced_state.combo { Combo::None => unreachable!(), Combo::SynthesisBegin => { - self.build_child_front(reduced_state, Action::MuscleMemory); - self.build_child_front(reduced_state, Action::Reflect); - self.build_child_front(reduced_state, Action::TrainedEye); + self.build_child_front(reduced_state, Action::MuscleMemory)?; + self.build_child_front(reduced_state, Action::Reflect)?; + self.build_child_front(reduced_state, Action::TrainedEye)?; } Combo::BasicTouch => { if !reduced_state.progress_only { - self.build_child_front(reduced_state, Action::RefinedTouch); + self.build_child_front(reduced_state, Action::RefinedTouch)?; } } Combo::StandardTouch => unreachable!(), } + + Some(()) } - fn build_child_front(&mut self, reduced_state: ReducedState, action: Action) { + fn build_child_front(&mut self, reduced_state: ReducedState, action: Action) -> Option<()> { + if self.interrupt_signal.is_set() { + return None; + } + if let Ok(new_full_state) = reduced_state .to_state() @@ -193,7 +216,7 @@ impl StepLowerBoundSolver { if new_reduced_state.steps_budget != 0 && new_reduced_state.durability > 0 { match self.solved_states.get(&new_reduced_state) { Some(id) => self.pareto_front_builder.push_from_id(*id), - None => self.solve_state(new_reduced_state), + None => self.solve_state(new_reduced_state)?, } self.pareto_front_builder.map(move |value| { value.first += action_progress; @@ -207,6 +230,8 @@ impl StepLowerBoundSolver { self.pareto_front_builder.merge(); } } + + Some(()) } } @@ -219,7 +244,9 @@ mod tests { fn solve(settings: Settings, actions: &[Action]) -> u8 { let state = SimulationState::from_macro(&settings, actions).unwrap(); - StepLowerBoundSolver::new(settings, false, false).step_lower_bound(state) + StepLowerBoundSolver::new(settings, false, false, AtomicFlag::new()) + .step_lower_bound(state) + .unwrap() } #[test] @@ -695,15 +722,15 @@ mod tests { /// Test that the upper-bound solver is monotonic, /// i.e. the quality UB of a state is never less than the quality UB of any of its children. fn monotonic_fuzz_check(settings: Settings) { - let mut solver = StepLowerBoundSolver::new(settings, false, false); + let mut solver = StepLowerBoundSolver::new(settings, false, false, AtomicFlag::new()); for _ in 0..10000 { let state = random_state(&settings); - let state_lower_bound = solver.step_lower_bound(state); + let state_lower_bound = solver.step_lower_bound(state).unwrap(); for action in settings.allowed_actions.actions_iter() { let child_lower_bound = match state.use_action(action, Condition::Normal, &settings) { Ok(child) => match child.is_final(&settings) { - false => solver.step_lower_bound(child), + false => solver.step_lower_bound(child).unwrap(), true if child.progress >= settings.max_progress && child.quality >= settings.max_quality => { diff --git a/solvers/src/utils/atomic_flag.rs b/solvers/src/utils/atomic_flag.rs new file mode 100644 index 0000000..0413210 --- /dev/null +++ b/solvers/src/utils/atomic_flag.rs @@ -0,0 +1,60 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +#[derive(Clone, Debug, Default)] +pub struct AtomicFlag { + flag: Arc, +} + +// https://users.rust-lang.org/t/compiler-hint-for-unlikely-likely-for-if-branches/62102/3 +#[inline] +#[cold] +fn cold() {} + +#[inline] +fn unlikely(b: bool) -> bool { + if b { + cold() + } + b +} + +impl AtomicFlag { + pub fn new() -> Self { + Self { + flag: Arc::new(AtomicBool::new(false)), + } + } + + pub fn set(&self) { + self.flag.store(true, Ordering::Relaxed); + } + + #[inline] + pub fn is_set(&self) -> bool { + unlikely(self.flag.load(Ordering::Relaxed)) + } + + pub fn clear(&self) { + self.flag.store(false, Ordering::SeqCst); + } +} + +#[cfg(test)] +mod tests { + use super::AtomicFlag; + + #[test] + fn test_atomic_flag() { + let flag = AtomicFlag::new(); + assert!(!flag.is_set()); + + flag.set(); + assert!(flag.is_set()); + + flag.clear(); + assert!(!flag.is_set()); + } +} diff --git a/solvers/src/utils/mod.rs b/solvers/src/utils/mod.rs index fd8bfd0..d1ffdbe 100644 --- a/solvers/src/utils/mod.rs +++ b/solvers/src/utils/mod.rs @@ -1,4 +1,7 @@ +mod atomic_flag; mod pareto_front_builder; + +pub use atomic_flag::AtomicFlag; pub use pareto_front_builder::{ParetoFrontBuilder, ParetoFrontId, ParetoValue}; pub struct NamedTimer { diff --git a/src/app.rs b/src/app.rs index 5f8756d..06c3c12 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,7 +17,7 @@ use egui::{ }; use game_data::{action_name, get_initial_quality, get_job_name, Consumable, Locale}; -use simulator::{Action, ActionImpl, HeartAndSoul, Manipulation, QuickInnovation}; +use simulator::{Action, ActionImpl, HeartAndSoul, Manipulation, QuickInnovation, Settings}; use crate::config::{CrafterConfig, QualitySource, QualityTarget, RecipeConfiguration}; use crate::widgets::*; @@ -30,6 +30,12 @@ fn load(cc: &eframe::CreationContext<'_>, key: &'static str } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum SolverInput { + Start(Settings, SolverConfig), + Cancel, +} + #[derive(Debug, Serialize, Deserialize)] pub enum SolverEvent { Progress(usize), @@ -62,6 +68,7 @@ pub struct MacroSolverApp { stats_edit_window_open: bool, actions: Vec, solver_pending: bool, + solver_interrupt_pending: bool, solver_progress: usize, start_time: Option, duration: Option, @@ -143,6 +150,7 @@ impl MacroSolverApp { stats_edit_window_open: false, actions: Vec::new(), solver_pending: false, + solver_interrupt_pending: false, solver_progress: 0, start_time: None, duration: None, @@ -309,11 +317,10 @@ impl eframe::App for MacroSolverApp { ); }); }); - ui.add_enabled_ui(!self.solver_pending, |ui| { - ui.group(|ui| { - ui.set_height(560.0); - self.draw_configuration_widget(ui) - }); + + ui.group(|ui| { + ui.set_height(560.0); + self.draw_config_and_results_widget(ui) }); }); }); @@ -360,13 +367,11 @@ impl eframe::App for MacroSolverApp { impl MacroSolverApp { fn solver_update(&mut self) { #[cfg(not(target_arch = "wasm32"))] - if let Some(bridge_rx) = &self.bridge.rx { - if let Ok(update) = bridge_rx.try_recv() { - match update { - SolverEvent::Progress(_) => self.data_update.progress_update.set(Some(update)), - SolverEvent::IntermediateSolution(_) | SolverEvent::FinalSolution(_) => { - self.data_update.solution_update.set(Some(update)) - } + if let Ok(update) = self.bridge.rx.try_recv() { + match update { + SolverEvent::Progress(_) => self.data_update.progress_update.set(Some(update)), + SolverEvent::IntermediateSolution(_) | SolverEvent::FinalSolution(_) => { + self.data_update.solution_update.set(Some(update)) } } } @@ -398,290 +403,296 @@ impl MacroSolverApp { } } - fn draw_configuration_widget(&mut self, ui: &mut egui::Ui) { + fn draw_config_and_results_widget(&mut self, ui: &mut egui::Ui) { ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Configuration").strong()); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.style_mut().spacing.item_spacing = [4.0, 4.0].into(); - if ui.button("Edit").clicked() { - self.stats_edit_window_open = true; - } - egui::ComboBox::from_id_source("SELECTED_JOB") - .width(20.0) - .selected_text(get_job_name(self.crafter_config.selected_job, self.locale)) - .show_ui(ui, |ui| { - for i in 0..8 { - ui.selectable_value( - &mut self.crafter_config.selected_job, - i, - get_job_name(i, self.locale), - ); - } - }); - }); - }); - ui.separator(); - - ui.label(egui::RichText::new(t!("label.crafter_stats")).strong()); - ui.horizontal(|ui| { - ui.label(format!("{}:", t!("craftsmanship"))); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add_enabled( - false, - egui::DragValue::new(&mut game_data::craftsmanship_bonus( - self.crafter_config.active_stats().craftsmanship, - &[self.selected_food, self.selected_potion], - )), - ); - ui.monospace("+"); - ui.add( - egui::DragValue::new( - &mut self.crafter_config.active_stats_mut().craftsmanship, - ) - .clamp_range(0..=9999), - ); - }); + ui.add_enabled_ui(!self.solver_pending, |ui| { + self.draw_configuration_widget(ui); }); - ui.horizontal(|ui| { - ui.label(format!("{}:", t!("control"))); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add_enabled( - false, - egui::DragValue::new(&mut game_data::control_bonus( - self.crafter_config.active_stats().control, - &[self.selected_food, self.selected_potion], - )), - ); - ui.monospace("+"); - ui.add( - egui::DragValue::new(&mut self.crafter_config.active_stats_mut().control) - .clamp_range(0..=9999), - ); - }); - }); - ui.horizontal(|ui| { - ui.label(format!("{}:", t!("cp"))); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add_enabled( - false, - egui::DragValue::new(&mut game_data::cp_bonus( - self.crafter_config.active_stats().cp, - &[self.selected_food, self.selected_potion], - )), - ); - ui.monospace("+"); - ui.add( - egui::DragValue::new(&mut self.crafter_config.active_stats_mut().cp) - .clamp_range(0..=999), - ); - }); - }); - ui.horizontal(|ui| { - ui.label(format!("{}:", t!("job_level"))); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add( - egui::DragValue::new(&mut self.crafter_config.active_stats_mut().level) - .clamp_range(1..=100), - ); - }); - }); - ui.separator(); - - ui.label(egui::RichText::new(t!("label.hq_materials")).strong()); - let mut has_hq_ingredient = false; - let recipe_ingredients = self.recipe_config.recipe.ingredients; - if let QualitySource::HqMaterialList(provided_ingredients) = - &mut self.recipe_config.quality_source - { - for (index, ingredient) in recipe_ingredients.into_iter().enumerate() { - if let Some(item) = game_data::ITEMS.get(&ingredient.item_id) { - if item.can_be_hq { - has_hq_ingredient = true; - ui.horizontal(|ui| { - ui.add(ItemNameLabel::new(ingredient.item_id, false, self.locale)); - ui.with_layout( - Layout::right_to_left(Align::Center), - |ui: &mut egui::Ui| { - let mut max_placeholder = ingredient.amount; - ui.add_enabled( - false, - egui::DragValue::new(&mut max_placeholder), - ); - ui.monospace("/"); - ui.add( - egui::DragValue::new(&mut provided_ingredients[index]) - .clamp_range(0..=ingredient.amount), - ); - }, - ); - }); - } - } + ui.add_space(5.5); + self.draw_results_widget(ui); + }); + } + + fn draw_configuration_widget(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Configuration").strong()); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.style_mut().spacing.item_spacing = [4.0, 4.0].into(); + if ui.button("Edit").clicked() { + self.stats_edit_window_open = true; } - } - if !has_hq_ingredient { - ui.label(t!("label.none")); - } - ui.separator(); + egui::ComboBox::from_id_source("SELECTED_JOB") + .width(20.0) + .selected_text(get_job_name(self.crafter_config.selected_job, self.locale)) + .show_ui(ui, |ui| { + for i in 0..8 { + ui.selectable_value( + &mut self.crafter_config.selected_job, + i, + get_job_name(i, self.locale), + ); + } + }); + }); + }); + ui.separator(); - ui.label(egui::RichText::new(t!("label.actions")).strong()); - if self.crafter_config.active_stats().level >= Manipulation::LEVEL_REQUIREMENT { - ui.add(egui::Checkbox::new( - &mut self.crafter_config.active_stats_mut().manipulation, - format!("{}", action_name(Action::Manipulation, self.locale)), - )); - } else { + ui.label(egui::RichText::new(t!("label.crafter_stats")).strong()); + ui.horizontal(|ui| { + ui.label(format!("{}:", t!("craftsmanship"))); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.add_enabled( false, - egui::Checkbox::new( - &mut false, - format!("{}", action_name(Action::Manipulation, self.locale)), - ), + egui::DragValue::new(&mut game_data::craftsmanship_bonus( + self.crafter_config.active_stats().craftsmanship, + &[self.selected_food, self.selected_potion], + )), ); - } - if self.crafter_config.active_stats().level >= HeartAndSoul::LEVEL_REQUIREMENT { - ui.add(egui::Checkbox::new( - &mut self.crafter_config.active_stats_mut().heart_and_soul, - format!("{}", action_name(Action::HeartAndSoul, self.locale)), - )); - } else { - ui.add_enabled( - false, - egui::Checkbox::new( - &mut false, - format!("{}", action_name(Action::HeartAndSoul, self.locale)), - ), + ui.monospace("+"); + ui.add( + egui::DragValue::new(&mut self.crafter_config.active_stats_mut().craftsmanship) + .clamp_range(0..=9999), ); - } - if self.crafter_config.active_stats().level >= QuickInnovation::LEVEL_REQUIREMENT { - ui.add(egui::Checkbox::new( - &mut self.crafter_config.active_stats_mut().quick_innovation, - format!("{}", action_name(Action::QuickInnovation, self.locale)), - )); - } else { + }); + }); + ui.horizontal(|ui| { + ui.label(format!("{}:", t!("control"))); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.add_enabled( false, - egui::Checkbox::new( - &mut false, - format!("{}", action_name(Action::QuickInnovation, self.locale)), - ), + egui::DragValue::new(&mut game_data::control_bonus( + self.crafter_config.active_stats().control, + &[self.selected_food, self.selected_potion], + )), ); - } - ui.separator(); - - ui.label(egui::RichText::new(t!("label.solver_settings")).strong()); - ui.horizontal(|ui| { - ui.label(t!("label.target_quality")); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.style_mut().spacing.item_spacing = [4.0, 4.0].into(); - let game_settings = game_data::get_game_settings( - self.recipe_config.recipe, - self.crafter_config.crafter_stats - [self.crafter_config.selected_job as usize], - self.selected_food, - self.selected_potion, - self.solver_config.adversarial, - ); - let mut current_value = self - .solver_config - .quality_target - .get_target(game_settings.max_quality); - match &mut self.solver_config.quality_target { - QualityTarget::Custom(value) => { - ui.add(egui::DragValue::new(value)); - } - _ => { - ui.add_enabled(false, egui::DragValue::new(&mut current_value)); - } - }; - egui::ComboBox::from_id_source("TARGET_QUALITY") - .selected_text(format!("{}", self.solver_config.quality_target)) - .show_ui(ui, |ui| { - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::Zero, - format!("{}", QualityTarget::Zero), - ); - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::CollectableT1, - format!("{}", QualityTarget::CollectableT1), - ); - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::CollectableT2, - format!("{}", QualityTarget::CollectableT2), - ); - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::CollectableT3, - format!("{}", QualityTarget::CollectableT3), - ); - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::Full, - format!("{}", QualityTarget::Full), - ); - ui.selectable_value( - &mut self.solver_config.quality_target, - QualityTarget::Custom(current_value), - format!("{}", QualityTarget::Custom(0)), - ) - }); - }); - }); - - ui.horizontal(|ui| { - ui.checkbox( - &mut self.solver_config.backload_progress, - t!("label.backload_progress"), + ui.monospace("+"); + ui.add( + egui::DragValue::new(&mut self.crafter_config.active_stats_mut().control) + .clamp_range(0..=9999), ); - ui.add(HelpText::new(t!("info.backload_progress"))); }); - - if self.recipe_config.recipe.is_expert { - self.solver_config.adversarial = false; - } - ui.horizontal(|ui| { + }); + ui.horizontal(|ui| { + ui.label(format!("{}:", t!("cp"))); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.add_enabled( - !self.recipe_config.recipe.is_expert, - egui::Checkbox::new( - &mut self.solver_config.adversarial, - t!("label.adversarial"), - ), + false, + egui::DragValue::new(&mut game_data::cp_bonus( + self.crafter_config.active_stats().cp, + &[self.selected_food, self.selected_potion], + )), + ); + ui.monospace("+"); + ui.add( + egui::DragValue::new(&mut self.crafter_config.active_stats_mut().cp) + .clamp_range(0..=999), ); - ui.add(HelpText::new(t!("info.adversarial"))); }); - if self.solver_config.adversarial { - ui.label( - egui::RichText::new(Self::experimental_warning_text()) - .small() - .color(ui.visuals().warn_fg_color), + }); + ui.horizontal(|ui| { + ui.label(format!("{}:", t!("job_level"))); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add( + egui::DragValue::new(&mut self.crafter_config.active_stats_mut().level) + .clamp_range(1..=100), ); + }); + }); + ui.separator(); + + ui.label(egui::RichText::new(t!("label.hq_materials")).strong()); + let mut has_hq_ingredient = false; + let recipe_ingredients = self.recipe_config.recipe.ingredients; + if let QualitySource::HqMaterialList(provided_ingredients) = + &mut self.recipe_config.quality_source + { + for (index, ingredient) in recipe_ingredients.into_iter().enumerate() { + if let Some(item) = game_data::ITEMS.get(&ingredient.item_id) { + if item.can_be_hq { + has_hq_ingredient = true; + ui.horizontal(|ui| { + ui.add(ItemNameLabel::new(ingredient.item_id, false, self.locale)); + ui.with_layout( + Layout::right_to_left(Align::Center), + |ui: &mut egui::Ui| { + let mut max_placeholder = ingredient.amount; + ui.add_enabled( + false, + egui::DragValue::new(&mut max_placeholder), + ); + ui.monospace("/"); + ui.add( + egui::DragValue::new(&mut provided_ingredients[index]) + .clamp_range(0..=ingredient.amount), + ); + }, + ); + }); + } + } } - - ui.horizontal(|ui| { - ui.checkbox( - &mut self.solver_config.minimize_steps, - t!("label.min_steps"), + } + if !has_hq_ingredient { + ui.label(t!("label.none")); + } + ui.separator(); + + ui.label(egui::RichText::new(t!("label.actions")).strong()); + if self.crafter_config.active_stats().level >= Manipulation::LEVEL_REQUIREMENT { + ui.add(egui::Checkbox::new( + &mut self.crafter_config.active_stats_mut().manipulation, + format!("{}", action_name(Action::Manipulation, self.locale)), + )); + } else { + ui.add_enabled( + false, + egui::Checkbox::new( + &mut false, + format!("{}", action_name(Action::Manipulation, self.locale)), + ), + ); + } + if self.crafter_config.active_stats().level >= HeartAndSoul::LEVEL_REQUIREMENT { + ui.add(egui::Checkbox::new( + &mut self.crafter_config.active_stats_mut().heart_and_soul, + format!("{}", action_name(Action::HeartAndSoul, self.locale)), + )); + } else { + ui.add_enabled( + false, + egui::Checkbox::new( + &mut false, + format!("{}", action_name(Action::HeartAndSoul, self.locale)), + ), + ); + } + if self.crafter_config.active_stats().level >= QuickInnovation::LEVEL_REQUIREMENT { + ui.add(egui::Checkbox::new( + &mut self.crafter_config.active_stats_mut().quick_innovation, + format!("{}", action_name(Action::QuickInnovation, self.locale)), + )); + } else { + ui.add_enabled( + false, + egui::Checkbox::new( + &mut false, + format!("{}", action_name(Action::QuickInnovation, self.locale)), + ), + ); + } + ui.separator(); + + ui.label(egui::RichText::new(t!("label.solver_settings")).strong()); + ui.horizontal(|ui| { + ui.label(t!("label.target_quality")); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.style_mut().spacing.item_spacing = [4.0, 4.0].into(); + let game_settings = game_data::get_game_settings( + self.recipe_config.recipe, + self.crafter_config.crafter_stats[self.crafter_config.selected_job as usize], + self.selected_food, + self.selected_potion, + self.solver_config.adversarial, ); - ui.add(HelpText::new(t!("info.min_steps"))); + let mut current_value = self + .solver_config + .quality_target + .get_target(game_settings.max_quality); + match &mut self.solver_config.quality_target { + QualityTarget::Custom(value) => { + ui.add(egui::DragValue::new(value)); + } + _ => { + ui.add_enabled(false, egui::DragValue::new(&mut current_value)); + } + }; + egui::ComboBox::from_id_source("TARGET_QUALITY") + .selected_text(format!("{}", self.solver_config.quality_target)) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::Zero, + format!("{}", QualityTarget::Zero), + ); + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::CollectableT1, + format!("{}", QualityTarget::CollectableT1), + ); + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::CollectableT2, + format!("{}", QualityTarget::CollectableT2), + ); + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::CollectableT3, + format!("{}", QualityTarget::CollectableT3), + ); + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::Full, + format!("{}", QualityTarget::Full), + ); + ui.selectable_value( + &mut self.solver_config.quality_target, + QualityTarget::Custom(current_value), + format!("{}", QualityTarget::Custom(0)), + ) + }); }); - if self.solver_config.minimize_steps { - ui.label( - egui::RichText::new(Self::experimental_warning_text()) - .small() - .color(ui.visuals().warn_fg_color), - ); - } + }); - ui.add_space(5.5); - ui.horizontal(|ui| { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.horizontal(|ui| { + ui.checkbox( + &mut self.solver_config.backload_progress, + t!("label.backload_progress"), + ); + ui.add(HelpText::new(t!("info.backload_progress"))); + }); + + if self.recipe_config.recipe.is_expert { + self.solver_config.adversarial = false; + } + ui.horizontal(|ui| { + ui.add_enabled( + !self.recipe_config.recipe.is_expert, + egui::Checkbox::new(&mut self.solver_config.adversarial, t!("label.adversarial")), + ); + ui.add(HelpText::new(t!("info.adversarial"))); + }); + if self.solver_config.adversarial { + ui.label( + egui::RichText::new(Self::experimental_warning_text()) + .small() + .color(ui.visuals().warn_fg_color), + ); + } + + ui.horizontal(|ui| { + ui.checkbox( + &mut self.solver_config.minimize_steps, + t!("label.min_steps"), + ); + ui.add(HelpText::new(t!("info.min_steps"))); + }); + if self.solver_config.minimize_steps { + ui.label( + egui::RichText::new(Self::experimental_warning_text()) + .small() + .color(ui.visuals().warn_fg_color), + ); + } + } + + fn draw_results_widget(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if !self.solver_pending { if ui.button(t!("label.solve")).clicked() { self.actions = Vec::new(); self.solver_pending = true; + self.solver_interrupt_pending = false; self.solver_progress = 0; self.start_time = Some(Instant::now()); let mut game_settings = game_data::get_game_settings( @@ -711,31 +722,43 @@ impl MacroSolverApp { }); game_settings.max_quality = target_quality.saturating_sub(initial_quality); - self.bridge.send((game_settings, self.solver_config)); + self.bridge + .send(SolverInput::Start(game_settings, self.solver_config)); log::debug!("{game_settings:?}"); } - if self.solver_pending { - ui.spinner(); - if self.solver_progress == 0 { - ui.label("Populating DP tables"); - } else { - // format with thousands separator - let num = self - .solver_progress - .to_string() - .as_bytes() - .rchunks(3) - .rev() - .map(std::str::from_utf8) - .collect::, _>>() - .unwrap() - .join(","); - ui.label(format!("{} nodes visited", num)); - } - } else if let Some(duration) = self.duration { + if let Some(duration) = self.duration { ui.label(format!("Time: {:.3}s", duration.as_secs_f64())); } - }); + } else { + #[cfg(not(target_arch = "wasm32"))] + ui.add_enabled_ui( + !self.solver_interrupt_pending && self.solver_pending, + |ui| { + if ui.button(t!("label.cancel")).clicked() { + self.bridge.send(SolverInput::Cancel); + self.solver_interrupt_pending = true; + } + }, + ); + + ui.spinner(); + if self.solver_progress == 0 { + ui.label("Populating DP tables"); + } else { + // format with thousands separator + let num = self + .solver_progress + .to_string() + .as_bytes() + .rchunks(3) + .rev() + .map(std::str::from_utf8) + .collect::, _>>() + .unwrap() + .join(","); + ui.label(format!("{} nodes visited", num)); + } + } }); }); } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index f972731..730854c 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -1,7 +1,7 @@ -use crate::app::{SolverConfig, SolverEvent}; -use simulator::{Action, Settings, SimulationState}; -use solvers::test_utils; -use std::sync::mpsc::Sender; +use crate::app::{SolverEvent, SolverInput}; +use simulator::{Action, SimulationState}; +use solvers::{test_utils, AtomicFlag}; +use std::sync::{mpsc::Sender, LazyLock}; #[cfg(not(target_arch = "wasm32"))] pub mod native; @@ -21,7 +21,7 @@ use gloo_worker::WorkerBridge; #[cfg(target_arch = "wasm32")] pub type BridgeType = WorkerBridge; -type Input = (Settings, SolverConfig); +type Input = SolverInput; type Output = SolverEvent; pub struct Worker { @@ -29,6 +29,8 @@ pub struct Worker { tx: Option>, } +static INTERRUPT_SIGNAL: LazyLock = LazyLock::new(AtomicFlag::new); + impl Worker { #[allow(unused)] pub fn solver_callback( @@ -43,64 +45,72 @@ impl Worker { input.unwrap() }; - let settings = input.0; - let config = input.1; + match input { + SolverInput::Start(settings, config) => { + INTERRUPT_SIGNAL.clear(); - let tx = self.tx.clone(); - let solution_callback = move |actions: &[Action]| { - self.send_event( - tx.clone(), - scope, - id, - SolverEvent::IntermediateSolution(actions.to_vec()), - ); - }; + let tx = self.tx.clone(); + let solution_callback = move |actions: &[Action]| { + self.send_event( + tx.clone(), + scope, + id, + SolverEvent::IntermediateSolution(actions.to_vec()), + ); + }; - let tx = self.tx.clone(); - let progress_callback = move |progress: usize| { - self.send_event(tx.clone(), scope, id, SolverEvent::Progress(progress)); - }; + let tx = self.tx.clone(); + let progress_callback = move |progress: usize| { + self.send_event(tx.clone(), scope, id, SolverEvent::Progress(progress)); + }; - let mut solution = if config.minimize_steps { - None // skip unsound solver - } else { - solvers::MacroSolver::new( - settings, - true, - true, - Box::new(solution_callback.clone()), - Box::new(progress_callback.clone()), - ) - .solve(SimulationState::new(&settings)) - }; + let mut solution = if config.minimize_steps { + None // skip unsound solver + } else { + solvers::MacroSolver::new( + settings, + true, + true, + Box::new(solution_callback.clone()), + Box::new(progress_callback.clone()), + INTERRUPT_SIGNAL.clone(), + ) + .solve(SimulationState::new(&settings)) + }; - if solution.is_none() - || test_utils::get_quality(&settings, solution.as_ref().unwrap().as_slice()) - < settings.max_quality - { - progress_callback(0); // reset solver progress - solution = solvers::MacroSolver::new( - settings, - config.backload_progress, - false, - Box::new(solution_callback), - Box::new(progress_callback), - ) - .solve(SimulationState::new(&settings)); - } + if solution.is_none() + || test_utils::get_quality(&settings, solution.as_ref().unwrap().as_slice()) + < settings.max_quality + { + progress_callback(0); // reset solver progress + solution = solvers::MacroSolver::new( + settings, + config.backload_progress, + false, + Box::new(solution_callback), + Box::new(progress_callback), + INTERRUPT_SIGNAL.clone(), + ) + .solve(SimulationState::new(&settings)); + } - let tx = self.tx.clone(); - match solution { - Some(actions) => { - self.send_event(tx.clone(), scope, id, SolverEvent::FinalSolution(actions)); + let tx = self.tx.clone(); + match solution { + Some(actions) => { + self.send_event(tx.clone(), scope, id, SolverEvent::FinalSolution(actions)); + } + None => { + self.send_event( + tx.clone(), + scope, + id, + SolverEvent::FinalSolution(Vec::new()), + ); + } + } } - None => { - self.send_event( - tx.clone(), - scope, - id, - SolverEvent::FinalSolution(Vec::new()), - ); + SolverInput::Cancel => { + INTERRUPT_SIGNAL.set(); } } } diff --git a/src/worker/native.rs b/src/worker/native.rs index af51b45..7e44061 100644 --- a/src/worker/native.rs +++ b/src/worker/native.rs @@ -12,23 +12,21 @@ impl DummyScope { } pub struct NativeBridge { - pub(crate) rx: Option>, + pub(crate) tx: Sender, + pub(crate) rx: Receiver, } impl NativeBridge { pub fn new() -> Self { - Self { rx: None } + let (tx, rx) = mpsc::channel::(); + Self { tx, rx } } pub fn send(&mut self, input: Input) { - let (tx, rx) = mpsc::channel::(); - - let worker = Worker::new(input, tx); + let worker = Worker::new(input, self.tx.clone()); std::thread::spawn(move || { worker.solver_callback(None, None, None); }); - - self.rx = Some(rx); } }