use crate::keybinds::{Binding, Keybinds};
#[cfg(feature = "plugins")]
use crate::plugin::{plugin_task, PluginErr, PluginErrTy, PluginMsg};
use crate::render::RenderState;
use anyhow::{bail, Result};
use mist_core::{
config::Config,
dialogs,
error::MistError::Str,
timer::{
dump::StateDump,
state::{RunState, RunUpdate, StateChangeRequest},
Run,
},
};
use sdl2::{
event::{Event, WindowEvent},
keyboard::Keycode,
ttf::Font,
};
#[cfg(feature = "icon")]
use sdl2::{image::ImageRWops, rwops::RWops};
#[cfg(feature = "plugins")]
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::{
cell::RefCell,
env,
fs::File,
path::PathBuf,
rc::Rc,
thread,
time::{Duration, Instant},
};
pub struct App<'a, 'b> {
context: sdl2::Sdl,
run: Rc<RefCell<Run>>,
ren_state: RenderState<'a, 'b>,
run_state: RunState,
config: Config,
msf: PathBuf,
no_file: bool,
#[cfg(feature = "plugins")]
plugin_tx: SyncSender<PluginMsg>,
#[cfg(feature = "plugins")]
scrq_rx: Receiver<StateChangeRequest>,
#[cfg(feature = "plugins")]
plug_err_rx: Receiver<PluginErr>,
}
static ONE_SIXTIETH: Duration = Duration::new(0, 1_000_000_000 / 60);
impl<'a, 'b> App<'a, 'b> {
pub fn init(
context: sdl2::Sdl,
timer_font: Font<'b, 'a>,
splits_font: Font<'b, 'a>,
) -> Result<Self> {
let video = context.video().map_err(Str)?;
let mut config = match Config::open() {
Ok(c) => c,
Err(e) => {
bail!(e);
}
};
let mut window = video
.window("mist", config.win_size().0, config.win_size().1)
.position_centered()
.resizable()
.build()?;
#[cfg(feature = "icon")]
{
let rw = RWops::from_bytes(include_bytes!("../assets/MIST.png")).unwrap(); window.set_icon(rw.load_png().map_err(Str)?);
}
let mut canvas = window.into_canvas().build()?;
let cmd_path = env::args().nth(1);
let not_exist = if let Some(ref x) = cmd_path {
!PathBuf::from(x).exists()
} else {
false
};
let mut path = if cmd_path.is_none() || not_exist {
if not_exist {
println!(
"File {} does not exist; falling back to configuration file!",
cmd_path.unwrap()
);
}
if let Some(x) = config.file() {
x.to_owned()
} else {
match dialogs::get_run_path() {
Some(x) => x.into(),
None => PathBuf::new(),
}
}
} else {
cmd_path.unwrap().into()
};
let (run, no_file) = loop {
if path == PathBuf::new() {
break (Run::empty(), true);
} else {
let msf = File::open(&path)?;
match Run::from_reader_msf(msf) {
Ok(r) => {
config.set_file(&path);
break (r, false);
}
Err(e) => {
if !dialogs::try_again(e) {
break (Run::empty(), true);
}
}
}
}
path = match dialogs::get_run_path() {
Some(x) => x.into(),
None => PathBuf::new(),
}
};
#[cfg(feature = "plugins")]
let (plugin_tx, scrq_rx, plug_err_rx) = {
let (plugin_tx, plugin_rx) = mpsc::sync_channel(1);
let (scrq_tx, scrq_rx) = mpsc::channel();
let (plug_err_tx, plug_err_rx) = mpsc::channel();
if config.plugins() {
let r2 = run.clone();
let _ = thread::Builder::new()
.name("plugins".into())
.spawn(move || plugin_task(r2, plugin_rx, scrq_tx, plug_err_tx));
}
(plugin_tx, scrq_rx, plug_err_rx)
};
let run = Rc::new(RefCell::new(run));
canvas.window_mut().set_title(&format!(
"mist: {} ({})",
run.borrow().game_title(),
run.borrow().category(),
))?;
let app = App {
context,
ren_state: RenderState::new(Rc::clone(&run), canvas, &config, timer_font, splits_font)?,
run_state: RunState::new(Rc::clone(&run)),
config,
msf: path,
run,
no_file,
#[cfg(feature = "plugins")]
plugin_tx,
#[cfg(feature = "plugins")]
scrq_rx,
#[cfg(feature = "plugins")]
plug_err_rx,
};
Ok(app)
}
pub fn run(mut self) -> Result<()> {
let mut frame_time: Instant;
let mut binds = Keybinds::from_raw(self.config.binds())?;
let mut state_change_queue = vec![];
let mut update: RunUpdate;
let mut ev_pump = self.context.event_pump().map_err(Str)?;
'running: loop {
frame_time = Instant::now();
for event in ev_pump.poll_iter() {
#[cfg(debug_assertions)]
println!("{:?}", event);
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} if !self.config.confirm() || dialogs::confirm_exit() => break 'running,
Event::MouseWheel { y, .. } => {
self.ren_state.scroll(y);
}
Event::MouseButtonDown { mouse_btn: m, .. } => {
let m = m.into();
if binds.load_config == m && !self.run_state.is_running() {
match Config::open() {
Ok(c) => {
self.config = c;
self.ren_state = self.ren_state.reload_config(&self.config)?;
binds = Keybinds::from_raw(self.config.binds())?;
}
Err(e) => {
dialogs::fail(e);
}
}
} else {
state_change_queue.push(self.handle_input(m, &binds)?);
}
}
Event::KeyDown {
keycode: Some(k),
repeat: false,
..
} => {
let k = k.into();
if binds.load_config == k && !self.run_state.is_running() {
match Config::open() {
Ok(c) => {
self.config = c;
self.ren_state = self.ren_state.reload_config(&self.config)?;
binds = Keybinds::from_raw(self.config.binds())?;
}
Err(e) => {
dialogs::fail(e);
}
}
} else {
state_change_queue.push(self.handle_input(k, &binds)?);
}
}
Event::Window {
win_event: WindowEvent::Resized(_, y),
..
} => {
self.ren_state.win_resize(y as u32);
}
_ => {}
}
}
#[cfg(feature = "plugins")]
while let Ok(rq) = self.scrq_rx.try_recv() {
state_change_queue.push(rq);
}
#[cfg(feature = "plugins")]
while let Ok(e) = self.plug_err_rx.try_recv() {
dialogs::error(&match e.ty {
PluginErrTy::Filesystem => format!("Could not load plugin file {}!", e.plugin),
PluginErrTy::WrongVersion(v) => {
let ve = version_to_readable(mist_pdk::pdk_version());
let vf = version_to_readable(v);
format!(
"Plugin {} version mismatch: expected {}.{}, found {}.{}!",
e.plugin, ve.0, ve.1, vf.0, vf.1
)
}
PluginErrTy::MissingSym(s) => format!(
"Could not load necessary symbol {} from plugin {}!",
s, e.plugin
),
});
}
update = self.run_state.update(&state_change_queue[..]);
state_change_queue.clear();
self.ren_state.update(&update)?;
#[cfg(feature = "plugins")]
let _ = self.plugin_tx.try_send(PluginMsg::Update(update));
self.ren_state.render()?;
if Instant::now().duration_since(frame_time) <= ONE_SIXTIETH {
thread::sleep(
ONE_SIXTIETH - Instant::now().duration_since(frame_time),
);
}
}
self.config.set_win_size(self.ren_state.win_size());
self.config.save()?;
if (self.run_state.needs_save() || self.no_file) && dialogs::save_check() {
if self.no_file || self.msf == PathBuf::new() {
let p = dialogs::get_save_as();
if let Some(s) = p {
self.msf = s.into();
let f = File::options()
.write(true)
.create(true)
.truncate(true)
.open(self.msf)?;
self.run.borrow().to_writer(f)?;
self.no_file = false;
}
} else {
let f = File::options()
.write(true)
.create(true)
.truncate(true)
.open(self.msf)?;
self.run.borrow().to_writer(f)?;
}
}
Ok(())
}
fn handle_input(&mut self, k: Binding, binds: &Keybinds) -> Result<StateChangeRequest> {
if binds.start_split == k {
Ok(StateChangeRequest::Split)
} else if binds.pause == k {
Ok(StateChangeRequest::Pause)
} else if binds.reset == k {
Ok(StateChangeRequest::Reset)
} else if binds.prev_comp == k {
Ok(StateChangeRequest::PrevComparison)
} else if binds.next_comp == k {
Ok(StateChangeRequest::NextComparison)
} else if binds.un_split == k {
Ok(StateChangeRequest::Unsplit)
} else if binds.skip_split == k {
Ok(StateChangeRequest::Skip)
} else if !self.run_state.is_running() {
if binds.load_splits == k {
if (self.run_state.needs_save() || self.no_file) && dialogs::save_check() {
if self.no_file || self.msf == PathBuf::new() {
let p = dialogs::get_save_as();
if let Some(s) = p {
self.msf = s.into();
let f = File::options()
.write(true)
.create(true)
.truncate(true)
.open(&self.msf)?;
self.run.borrow().to_writer(f)?;
self.no_file = false;
}
} else {
let f = File::options()
.write(true)
.create(true)
.truncate(true)
.open(&self.msf)?;
self.run.borrow().to_writer(f)?;
}
}
while let Some(x) = dialogs::get_run_path() {
self.msf = x.into();
let f = File::open(&self.msf)?;
match Run::from_reader_msf(f) {
Ok(r) => {
#[cfg(feature = "plugins")]
if self.config.plugins() {
let _ = self.plugin_tx.send(PluginMsg::Shutdown);
let (plugin_tx, plugin_rx) = mpsc::sync_channel(1);
let (scrq_tx, scrq_rx) = mpsc::channel();
let (plug_err_tx, plug_err_rx) = mpsc::channel();
let r2 = r.clone();
let _ = thread::Builder::new()
.name("plugins".into())
.spawn(|| plugin_task(r2, plugin_rx, scrq_tx, plug_err_tx));
self.plugin_tx = plugin_tx;
self.scrq_rx = scrq_rx;
self.plug_err_rx = plug_err_rx;
};
self.run.replace(r);
self.config.set_file(&self.msf);
self.run_state = RunState::new(Rc::clone(&self.run));
self.ren_state.reload_run()?;
break;
}
Err(e) => {
if !dialogs::try_again(e) {
break;
}
}
}
}
Ok(StateChangeRequest::None)
} else if binds.dump_state == k {
if let Some(p) = dialogs::get_dump_save() {
let d = self.run_state.create_state_dump();
d.write(p)?;
}
Ok(StateChangeRequest::None)
} else if binds.load_state == k {
while let Some(p) = dialogs::get_dump_path() {
match StateDump::open(p) {
Ok(d) => {
self.run_state.read_dump(&d);
self.ren_state.read_dump(&d)?;
break;
}
Err(e) => {
if !dialogs::try_again(e) {
break;
}
}
}
}
Ok(StateChangeRequest::None)
} else {
Ok(StateChangeRequest::None)
}
} else {
Ok(StateChangeRequest::None)
}
}
}
fn version_to_readable(v: u16) -> (u8, u8) {
(((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8)
}