diff --git a/.gitignore b/.gitignore index 0592392..40b79c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .DS_Store +.vscode/ diff --git a/Cargo.lock b/Cargo.lock index 7b058bf..60d641f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,6 +194,8 @@ dependencies = [ "livesplit-hotkey", "memoffset 0.9.1", "parking_lot", + "process-memory", + "read-process-memory", "rfd", "roxmltree", "serde", @@ -201,6 +203,7 @@ dependencies = [ "serde_json", "strum 0.27.1", "strum_macros 0.27.1", + "sysinfo", "thread-priority", "time", "toml", @@ -543,9 +546,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64-simd" @@ -775,7 +778,7 @@ version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -1279,12 +1282,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1547,9 +1550,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -1700,6 +1703,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1868,9 +1877,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d75c7014ddab93c232bc6bb9f64790d3dfd1d605199acd4b40b6d69e691e9f" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" dependencies = [ "byteorder-lite", "quick-error", @@ -1941,9 +1950,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" @@ -1980,9 +1989,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.173" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -2038,7 +2047,7 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "livesplit-core" version = "0.13.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ee36c2755d888184e93db31939562fbb2c715661" +source = "git+https://github.com/LiveSplit/livesplit-core#59a237fd59fc52133cef7e247888788871983110" dependencies = [ "base64-simd", "bytemuck", @@ -2071,7 +2080,7 @@ dependencies = [ [[package]] name = "livesplit-hotkey" version = "0.8.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ee36c2755d888184e93db31939562fbb2c715661" +source = "git+https://github.com/LiveSplit/livesplit-core#59a237fd59fc52133cef7e247888788871983110" dependencies = [ "bitflags 2.9.1", "cfg-if", @@ -2088,7 +2097,7 @@ dependencies = [ [[package]] name = "livesplit-title-abbreviations" version = "0.3.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ee36c2755d888184e93db31939562fbb2c715661" +source = "git+https://github.com/LiveSplit/livesplit-core#59a237fd59fc52133cef7e247888788871983110" [[package]] name = "lock_api" @@ -2106,6 +2115,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2290,6 +2308,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2317,18 +2344,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2523,6 +2551,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-surface" version = "0.3.1" @@ -2853,11 +2891,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process-memory" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9599c34fcc8067c3105dc746c0ce85e3ea61784568b8234179fad490b1dcc1" +dependencies = [ + "libc", + "mach", + "winapi", +] + [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "promising-future" @@ -2904,9 +2953,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -2995,6 +3044,18 @@ dependencies = [ "font-types", ] +[[package]] +name = "read-process-memory" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8497683b2f0b6887786f1928c118f26ecc6bb3d78bbb6ed23e8e7ba110af3bb0" +dependencies = [ + "libc", + "log", + "mach", + "winapi", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3367,7 +3428,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn", @@ -3427,7 +3488,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -3440,7 +3501,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -3460,9 +3521,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -3489,6 +3550,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", +] + [[package]] name = "tap" version = "1.0.1" @@ -3559,9 +3634,9 @@ dependencies = [ [[package]] name = "thread-priority" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e61777c6a097a7cd2247705eebd815ffde638f46e5cd9b776da609e41ac5228" +checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5" dependencies = [ "bitflags 2.9.1", "cfg-if", @@ -3717,9 +3792,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -4857,9 +4932,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +checksum = "635887f4315a33cb714eb059bdbd7c1c92bfa71bc5b9d5115460502f788c2ab5" [[package]] name = "xdg-home" @@ -5093,18 +5168,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", @@ -5173,9 +5248,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" +checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 24d5705..ad05d16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,10 @@ serde_json = "1" tungstenite = "0" livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features = ["software-rendering", "font-loading"] } +#livesplit-core = "0.13.0" livesplit-hotkey = { git = "https://github.com/LiveSplit/livesplit-core" } +#livesplit-hotkey = "0.8.0" +#livesplit-auto-splitting = "0.1.0" epaint = "0.31" eframe = { version = "0.31", features = ["glow"] } @@ -35,6 +38,9 @@ bytemuck = { version = "*", features = ["derive"] } memoffset = "*" thread-priority = "2" anyhow = "1" +sysinfo = "0.35.2" +process-memory = "0.5.0" +read-process-memory = "0.1.6" # Remember to test with --release [profile.dev] diff --git a/data/BattleToads b/data/BattleToads new file mode 100644 index 0000000..4043779 --- /dev/null +++ b/data/BattleToads @@ -0,0 +1,18 @@ +0x000D = count of levels beaten +0x0010 = level ID + +; enum level_names +intro: = 0 +ragnarok_canyon: = 1 +wookie_hole: = 2 +TurboTunnel: = 3 +arctic_caverns: = 4 +revolution: = 5 +volkmire_inferno: = 6 +intruder_excluder: = 7 +karnath_lair: = 8 +rat_race: = 9 +clinger_winger: = $A +terra_tubes: = $B +SurfCity: = $C +armageddon: = $D \ No newline at end of file diff --git a/src/autosplitters.rs b/src/autosplitters.rs index fdd6d1a..8cee72f 100644 --- a/src/autosplitters.rs +++ b/src/autosplitters.rs @@ -1,9 +1,24 @@ pub mod json; +pub mod nwa; pub mod supermetroid; - use anyhow::Result; use livesplit_core::TimeSpan; +#[derive(Debug, Copy, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +pub enum AType { + QUSB2SNES, + NWA, + ASL, + CUSTOM, +} + +#[derive(Debug, Copy, Clone)] +pub struct NWASummary { + pub start: bool, + pub reset: bool, + pub split: bool, +} + #[derive(Debug, Copy, Clone)] pub struct SNESSummary { pub latency_average: f32, diff --git a/src/autosplitters/nwa.rs b/src/autosplitters/nwa.rs new file mode 100644 index 0000000..a43766a --- /dev/null +++ b/src/autosplitters/nwa.rs @@ -0,0 +1,72 @@ +use crate::{autosplitters::NWASummary, config::app_config::AppConfig, nwa::NWASyncClient}; +use anyhow::Result; +use std::sync::Arc; + +pub mod battletoads; +pub mod supermetroid; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum Game { + Battletoads, + SuperMetroid, + // None, +} + +pub fn fill_drop_down(ui: &mut egui::Ui, game: &mut Game) { + ui.selectable_value(game, Game::Battletoads, "Battletoads"); + ui.selectable_value(game, Game::SuperMetroid, "Super Metroid"); +} + +pub fn nwaobject( + game: Game, + app_config: Arc>, + ip: &str, + port: u32, +) -> Box { + match game { + Game::Battletoads => Box::new(battletoads::BattletoadsAutoSplitter { + prior_level: 0, + level: 0, + reset_timer_on_game_reset: app_config + .read() + .unwrap() + .reset_timer_on_game_reset + .unwrap(), + client: NWASyncClient::connect(ip, port).unwrap(), + }), + Game::SuperMetroid => Box::new(supermetroid::SupermetroidAutoSplitter { + prior_state: 0, + state: 0, + prior_room_id: 0, + room_id: 0, + reset_timer_on_game_reset: app_config + .read() + .unwrap() + .reset_timer_on_game_reset + .unwrap(), + client: NWASyncClient::connect(ip, port).unwrap(), + }), + } +} + +pub trait Splitter { + fn client_id(&mut self); + + fn emu_info(&mut self); + + fn emu_game_info(&mut self); + + fn emu_status(&mut self); + + fn core_info(&mut self); + + fn core_memories(&mut self); + + fn update(&mut self) -> Result; + + fn start(&mut self) -> bool; + + fn reset(&mut self) -> bool; + + fn split(&mut self) -> bool; +} diff --git a/src/autosplitters/nwa/battletoads.rs b/src/autosplitters/nwa/battletoads.rs new file mode 100644 index 0000000..f94d1f8 --- /dev/null +++ b/src/autosplitters/nwa/battletoads.rs @@ -0,0 +1,118 @@ +use crate::autosplitters::nwa::Splitter; +use crate::autosplitters::NWASummary; +use crate::config::app_config::YesOrNo; +use crate::nwa; +use anyhow::Result; + +pub struct BattletoadsAutoSplitter { + pub prior_level: u8, + pub level: u8, + pub reset_timer_on_game_reset: YesOrNo, + pub client: nwa::NWASyncClient, +} + +impl Splitter for BattletoadsAutoSplitter { + fn client_id(&mut self) { + let cmd = "MY_NAME_IS"; + let args = Some("Annelid"); + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_info(&mut self) { + let cmd = "EMULATOR_INFO"; + let args = Some("0"); + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_game_info(&mut self) { + let cmd = "GAME_INFO"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_status(&mut self) { + let cmd = "EMULATION_STATUS"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn core_info(&mut self) { + let cmd = "CORE_CURRENT_INFO"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn core_memories(&mut self) { + let cmd = "CORE_MEMORIES"; + let args = None; + let _ = self.client.execute_command(cmd, args); + // println!("{summary:#?}"); + } + + fn update(&mut self) -> Result { + self.prior_level = self.level; + let cmd = "CORE_READ"; + let args = Some("RAM;$0010;1"); + let summary = self.client.execute_command(cmd, args).unwrap(); + // println!("{:#?}", summary); + match summary { + nwa::EmulatorReply::Binary(summary) => self.level = *summary.first().unwrap(), + nwa::EmulatorReply::Error(summary) => println!("{summary:?}"), + _ => println!("{summary:?}"), + } + // println!("{:#?}", level); + + let start = self.start(); + let reset = self.reset(); + let split = self.split(); + Ok(NWASummary { + start, + reset, + split, + }) + } + + fn start(&mut self) -> bool { + if self.level == 1 && self.prior_level == 0 { + return true; + } + false + } + + fn reset(&mut self) -> bool { + if self.level == 0 + && self.prior_level != 0 + && self.reset_timer_on_game_reset == YesOrNo::Yes + { + return true; + } + false + } + + fn split(&mut self) -> bool { + if self.level > self.prior_level && self.prior_level < 100 { + return true; + } + false + } +} + +// let cmd = "CORE_INFO"; +// let args = Some("quickerNES"); +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); + +// let cmd = "CORES_LIST"; +// let args = None; +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); + +// let cmd = "LIST_BIZHAWK_DOMAINS"; +// let args = None; +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); diff --git a/src/autosplitters/nwa/supermetroid.rs b/src/autosplitters/nwa/supermetroid.rs new file mode 100644 index 0000000..cbd426b --- /dev/null +++ b/src/autosplitters/nwa/supermetroid.rs @@ -0,0 +1,137 @@ +use crate::autosplitters::nwa::Splitter; +use crate::autosplitters::NWASummary; +use crate::config::app_config::YesOrNo; +use crate::nwa; +use anyhow::Result; + +pub struct SupermetroidAutoSplitter { + pub prior_state: u8, + pub state: u8, + pub prior_room_id: u16, + pub room_id: u16, + pub reset_timer_on_game_reset: YesOrNo, + pub client: nwa::NWASyncClient, +} + +impl Splitter for SupermetroidAutoSplitter { + fn client_id(&mut self) { + let cmd = "MY_NAME_IS"; + let args = Some("Annelid"); + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_info(&mut self) { + let cmd = "EMULATOR_INFO"; + let args = Some("0"); + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_game_info(&mut self) { + let cmd = "GAME_INFO"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn emu_status(&mut self) { + let cmd = "EMULATION_STATUS"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn core_info(&mut self) { + let cmd = "CORE_CURRENT_INFO"; + let args = None; + self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + } + + fn core_memories(&mut self) { + let cmd = "CORE_MEMORIES"; + let args = None; + let _ = self.client.execute_command(cmd, args); + // println!("{summary:#?}"); + } + + fn update(&mut self) -> Result { + // read memory for the game state + { + self.prior_state = self.state; + let cmd = "CORE_READ"; + let args = Some("WRAM;$0998;1"); + let summary = self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + match summary { + nwa::EmulatorReply::Binary(summary) => self.state = *summary.first().unwrap(), + nwa::EmulatorReply::Error(summary) => println!("{summary:?}"), + _ => println!("{summary:?}"), + } + // println!("{:#?}", self.state); + } + + // read memory for room + { + self.prior_room_id = self.room_id; + let cmd = "CORE_READ"; + let args = Some("WRAM;$079B;2"); + let summary = self.client.execute_command(cmd, args).unwrap(); + // println!("{summary:#?}"); + + match summary { + nwa::EmulatorReply::Binary(summary) => { + self.room_id = + // Have to reassemble the half word roomID + ((*summary.last().unwrap() as u16) << 8) | *summary.first().unwrap() as u16 + } + nwa::EmulatorReply::Error(summary) => println!("{summary:?}"), + _ => println!("{summary:?}"), + } + // println!("{:#?}", self.room_id); + } + + // TODO: add the other memory reads + + let start = self.start(); + let reset = self.reset(); + let split = self.split(); + Ok(NWASummary { + start, + reset, + split, + }) + } + + fn start(&mut self) -> bool { + self.state == 0x1F && self.prior_state == 0x1E + } + + fn reset(&mut self) -> bool { + self.room_id == 0 + && self.prior_room_id != 0 + && self.reset_timer_on_game_reset == YesOrNo::Yes + } + + fn split(&mut self) -> bool { + self.room_id == 0xDF45 && self.prior_state == 0x8 && self.state == 0x20 + + // TODO: add the rest of the splits + } +} + +// let cmd = "CORE_INFO"; +// let args = Some("quickerNES"); +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); + +// let cmd = "CORES_LIST"; +// let args = None; +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); + +// let cmd = "LIST_BIZHAWK_DOMAINS"; +// let args = None; +// let summary = client.execute_command(cmd, args); +// println!("{:#?}",summary); diff --git a/src/config/app_config.rs b/src/config/app_config.rs index 39d2a67..16e30bf 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -1,7 +1,9 @@ +use crate::autosplitters; use clap::Parser; use serde_derive::{Deserialize, Serialize}; use crate::hotkey::*; +use crate::utils::*; #[derive(Deserialize, Serialize, Parser, Debug, Clone)] #[clap(author, version, about, long_about = None)] @@ -38,6 +40,8 @@ pub struct AppConfig { pub hot_key_comparison_next: Option, #[clap(skip)] pub hot_key_comparison_prev: Option, + #[clap(skip)] + pub autosplitter_type: Option, } #[derive(clap::ValueEnum, Clone, Copy, Debug, Serialize, Deserialize, Default, PartialEq, Eq)] @@ -47,11 +51,29 @@ pub enum YesOrNo { No, } +impl From for YesOrNo { + fn from(b: bool) -> Self { + match b { + true => YesOrNo::Yes, + false => YesOrNo::No, + } + } +} + +impl From for bool { + fn from(yes: YesOrNo) -> Self { + match yes { + YesOrNo::Yes => true, + YesOrNo::No => false, + } + } +} + pub const DEFAULT_FRAME_RATE: f32 = 30.0; pub const DEFAULT_POLLING_RATE: f32 = 20.0; impl AppConfig { - fn new() -> Self { + pub fn new() -> Self { let modifiers = ::egui::Modifiers::default(); AppConfig { recent_splits: None, @@ -91,8 +113,69 @@ impl AppConfig { reset_timer_on_game_reset: Some(YesOrNo::No), reset_game_on_timer_reset: Some(YesOrNo::No), global_hotkeys: Some(YesOrNo::Yes), + autosplitter_type: Some(autosplitters::AType::QUSB2SNES), } } + + pub fn save_app_config(&self) { + use std::io::Write; + let project_dirs = directories::ProjectDirs::from("", "", "annelid") // get directories + .ok_or("Unable to load computer configuration directory"); + println!("project_dirs = {project_dirs:#?}"); + + let config_dir = project_dirs.unwrap(); // get preferences directory + println!("project_dirs = {:#?}", config_dir.preference_dir()); + + messagebox_on_error(|| { + std::fs::create_dir_all(config_dir.preference_dir()).expect("Created config dir"); // create preferences directory + + let mut config_path = config_dir.preference_dir().to_path_buf(); + config_path.push("settings.toml"); + + println!("Saving to {config_path:#?}"); + let f = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(config_path)?; + let mut writer = std::io::BufWriter::new(f); + let toml = toml::to_string_pretty(&self)?; + writer.write_all(toml.as_bytes())?; + writer.flush()?; + Ok(()) + }); + } + + pub fn load_app_config(mut self) -> Self { + let project_dirs = directories::ProjectDirs::from("", "", "annelid") // get directories + .ok_or("Unable to load computer configuration directory"); + println!("project_dirs = {project_dirs:#?}"); + + let config_dir = project_dirs.unwrap(); // get preferences directory + println!("project_dirs = {:#?}", config_dir.preference_dir()); + + messagebox_on_error(|| { + use std::io::Read; + let mut config_path = config_dir.preference_dir().to_path_buf(); + config_path.push("settings.toml"); + println!("Loading from {config_path:#?}"); + let saved_config: AppConfig = std::fs::File::open(config_path) + .and_then(|mut f| { + let mut buffer = String::new(); + f.read_to_string(&mut buffer)?; + match toml::from_str(&buffer) { + Ok(app_config) => Ok(app_config), + Err(e) => Err(from_de_error(e)), + } + + // }).unwrap; + }) + .unwrap_or_default(); + self = saved_config; + Ok(()) + }); + self + } } impl Default for AppConfig { diff --git a/src/hotkey.rs b/src/hotkey.rs index 5a40953..567f0cc 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -1,3 +1,6 @@ +use core::fmt; +use std::ops::BitOr; + use serde_derive::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Copy, Clone)] @@ -12,6 +15,118 @@ impl HotKey { } } +pub fn to_egui_keycode(key: livesplit_hotkey::KeyCode) -> ::egui::Key { + use livesplit_hotkey::KeyCode::*; + + match key { + ArrowDown => egui::Key::ArrowDown, + ArrowLeft => egui::Key::ArrowLeft, + ArrowRight => egui::Key::ArrowRight, + ArrowUp => egui::Key::ArrowUp, + Escape => egui::Key::Escape, + Tab => egui::Key::Tab, + Backspace => egui::Key::Backspace, + Enter => egui::Key::Enter, + Space => egui::Key::Space, + Insert => egui::Key::Insert, + Delete => egui::Key::Delete, + Home => egui::Key::Home, + End => egui::Key::End, + PageUp => egui::Key::PageUp, + PageDown => egui::Key::PageDown, + Numpad0 => egui::Key::Num0, + Numpad1 => egui::Key::Num1, + Numpad2 => egui::Key::Num2, + Numpad3 => egui::Key::Num3, + Numpad4 => egui::Key::Num4, + Numpad5 => egui::Key::Num5, + Numpad6 => egui::Key::Num6, + Numpad7 => egui::Key::Num7, + Numpad8 => egui::Key::Num8, + Numpad9 => egui::Key::Num9, + KeyA => egui::Key::A, + KeyB => egui::Key::B, + KeyC => egui::Key::C, + KeyD => egui::Key::D, + KeyE => egui::Key::E, + KeyF => egui::Key::F, + KeyG => egui::Key::G, + KeyH => egui::Key::H, + KeyI => egui::Key::I, + KeyJ => egui::Key::J, + KeyK => egui::Key::K, + KeyL => egui::Key::L, + KeyM => egui::Key::M, + KeyN => egui::Key::N, + KeyO => egui::Key::O, + KeyP => egui::Key::P, + KeyQ => egui::Key::Q, + KeyR => egui::Key::R, + KeyS => egui::Key::S, + KeyT => egui::Key::T, + KeyU => egui::Key::U, + KeyV => egui::Key::V, + KeyW => egui::Key::W, + KeyX => egui::Key::X, + KeyY => egui::Key::Y, + KeyZ => egui::Key::Z, + F1 => egui::Key::F1, + F2 => egui::Key::F2, + F3 => egui::Key::F3, + F4 => egui::Key::F4, + F5 => egui::Key::F5, + F6 => egui::Key::F6, + F7 => egui::Key::F7, + F8 => egui::Key::F8, + F9 => egui::Key::F9, + F10 => egui::Key::F10, + F11 => egui::Key::F11, + F12 => egui::Key::F12, + F13 => egui::Key::F13, + F14 => egui::Key::F14, + F15 => egui::Key::F15, + F16 => egui::Key::F16, + F17 => egui::Key::F17, + F18 => egui::Key::F18, + F19 => egui::Key::F19, + F20 => egui::Key::F20, + F21 => egui::Key::F21, + F22 => egui::Key::F22, + F23 => egui::Key::F23, + F24 => egui::Key::F24, + // F24 => egui::Key::F25, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F26, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F27, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F28, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F29, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F30, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F31, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F32, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F33, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F34, // TODO: hotkey lib doesn't support this yet + // F24 => egui::Key::F35, // TODO: hotkey lib doesn't support this yet + Minus => egui::Key::Minus, + // Equal => egui::Key::Plus, + Equal => egui::Key::Equals, + Copy => egui::Key::Copy, + Cut => egui::Key::Cut, + Paste => egui::Key::Paste, + Comma => egui::Key::Comma, + Backslash => egui::Key::Backslash, + Slash => egui::Key::Slash, + // Slash => egui::Key::Questionmark, + IntlBackslash => egui::Key::Pipe, + BracketLeft => egui::Key::OpenBracket, + BracketRight => egui::Key::CloseBracket, + Backquote => egui::Key::Backtick, + Period => egui::Key::Period, + Semicolon => egui::Key::Semicolon, + // Semicolon => egui::Key::Colon, + Quote => egui::Key::Quote, + _ => egui::Key::Comma, + } +} + pub fn to_livesplit_keycode(key: &::egui::Key) -> livesplit_hotkey::KeyCode { use livesplit_hotkey::KeyCode::*; @@ -144,6 +259,24 @@ pub fn to_livesplit_keycode_alternative(key: &::egui::Key) -> Option ::egui::Modifiers { + // use livesplit_hotkey::Modifiers; + let mut mods = ::egui::Modifiers::NONE; + if modifiers.contains(livesplit_hotkey::Modifiers::SHIFT) { + mods = ::egui::Modifiers::bitor(mods, ::egui::Modifiers::SHIFT); + }; + if modifiers.contains(livesplit_hotkey::Modifiers::CONTROL) { + mods = ::egui::Modifiers::bitor(mods, ::egui::Modifiers::CTRL); + }; + if modifiers.contains(livesplit_hotkey::Modifiers::ALT) { + mods = ::egui::Modifiers::bitor(mods, ::egui::Modifiers::ALT); + }; + if modifiers.contains(livesplit_hotkey::Modifiers::META) { + mods = ::egui::Modifiers::bitor(mods, ::egui::Modifiers::COMMAND); + }; + mods +} + pub fn to_livesplit_modifiers(modifiers: &::egui::Modifiers) -> livesplit_hotkey::Modifiers { use livesplit_hotkey::Modifiers; let mut mods = Modifiers::empty(); @@ -161,3 +294,30 @@ pub fn to_livesplit_modifiers(modifiers: &::egui::Modifiers) -> livesplit_hotkey }; mods } + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum HotkeyAction { + Start, + Reset, + Undo, + Skip, + Pause, + ComparisonPrevious, + ComparisonNext, + ToggleGlobalHotkeys, +} + +impl fmt::Display for HotkeyAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HotkeyAction::Start => write!(f, "Start / Split"), + HotkeyAction::Reset => write!(f, "Reset"), + HotkeyAction::Undo => write!(f, "Undo Split"), + HotkeyAction::Skip => write!(f, "Skip Split"), + HotkeyAction::Pause => write!(f, "Pause"), + HotkeyAction::ComparisonPrevious => write!(f, "Switch Comparison (Previous)"), + HotkeyAction::ComparisonNext => write!(f, "Switch Comparison (Next)"), + HotkeyAction::ToggleGlobalHotkeys => write!(f, "Toggle Global Hotkeys"), + } + } +} diff --git a/src/livesplit_renderer.rs b/src/livesplit_renderer.rs index 742c545..2577caf 100644 --- a/src/livesplit_renderer.rs +++ b/src/livesplit_renderer.rs @@ -1,12 +1,21 @@ -use crate::autosplitters::supermetroid::Settings; -use crate::autosplitters::supermetroid::SuperMetroidAutoSplitter; -use crate::autosplitters::AutoSplitter; +use crate::autosplitters::{ + self, + nwa::{fill_drop_down, nwaobject, Game}, + supermetroid::{Settings, SuperMetroidAutoSplitter}, + AutoSplitter, +}; use anyhow::{anyhow, Context, Result}; use eframe::egui; use livesplit_core::{Layout, SharedTimer, Timer}; use livesplit_hotkey::Hook; use parking_lot::RwLock; -use std::sync::Arc; +use std::{ + net::Ipv4Addr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; use thread_priority::{set_current_thread_priority, ThreadBuilder, ThreadPriority}; use crate::config::app_config::*; @@ -24,17 +33,20 @@ pub struct LiveSplitCoreRenderer { layout_state: Option, image_cache: livesplit_core::settings::ImageCache, timer: SharedTimer, - show_settings_editor: bool, - settings: Arc>, + settings: Arc>, can_exit: bool, is_exiting: bool, thread_chan: std::sync::mpsc::SyncSender, - project_dirs: directories::ProjectDirs, - pub app_config: std::sync::Arc>, + pub app_config: Arc>, app_config_processed: bool, glow_canvas: GlowCanvas, global_hotkey_hook: Option, load_errors: Vec, + show_edit_autosplitter_settings_dialog: Arc, + show_nwa_autosplitter_settings_dialog: Arc, + game: Game, + address: Arc>, + port: Arc>, } fn show_children( @@ -74,10 +86,8 @@ impl LiveSplitCoreRenderer { pub fn new( timer: SharedTimer, layout: Layout, - settings: Arc>, chan: std::sync::mpsc::SyncSender, - project_dirs: directories::ProjectDirs, - cli_config: AppConfig, + config: AppConfig, ) -> Self { LiveSplitCoreRenderer { timer, @@ -85,21 +95,24 @@ impl LiveSplitCoreRenderer { renderer: livesplit_core::rendering::software::BorrowedRenderer::new(), image_cache: livesplit_core::settings::ImageCache::new(), layout_state: None, - show_settings_editor: false, - settings, + settings: Arc::new(RwLock::new(autosplitters::supermetroid::Settings::new())), can_exit: false, is_exiting: false, thread_chan: chan, - project_dirs, - app_config: std::sync::Arc::new(std::sync::RwLock::new(cli_config)), + app_config: Arc::new(std::sync::RwLock::new(config)), app_config_processed: false, glow_canvas: GlowCanvas::new(), global_hotkey_hook: None, load_errors: vec![], + show_edit_autosplitter_settings_dialog: Arc::new(AtomicBool::new(false)), + show_nwa_autosplitter_settings_dialog: Arc::new(AtomicBool::new(false)), + game: Game::Battletoads, + address: Arc::new(RwLock::new(Ipv4Addr::new(0, 0, 0, 0).to_string())), + port: Arc::new(RwLock::new(48879)), } } - pub fn confirm_save(&mut self, gl: &std::sync::Arc) -> Result<()> { + pub fn confirm_save(&mut self, gl: &Arc) -> Result<()> { use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; let empty_path = "".to_owned(); let document_dir = match directories::UserDirs::new() { @@ -144,87 +157,6 @@ impl LiveSplitCoreRenderer { Ok(()) } - pub fn save_app_config(&self) { - messagebox_on_error(|| { - use std::io::Write; - let mut config_path = self.project_dirs.preference_dir().to_path_buf(); - config_path.push("settings.toml"); - println!("Saving to {config_path:#?}"); - let f = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(config_path)?; - let mut writer = std::io::BufWriter::new(f); - let toml = toml::to_string_pretty(&self.app_config)?; - writer.write_all(toml.as_bytes())?; - writer.flush()?; - Ok(()) - }); - } - - pub fn load_app_config(&mut self) { - messagebox_on_error(|| { - use std::io::Read; - let mut config_path = self.project_dirs.preference_dir().to_path_buf(); - config_path.push("settings.toml"); - println!("Loading from {config_path:#?}"); - let saved_config: AppConfig = std::fs::File::open(config_path) - .and_then(|mut f| { - let mut buffer = String::new(); - f.read_to_string(&mut buffer)?; - match toml::from_str(&buffer) { - Ok(app_config) => Ok(app_config), - Err(e) => Err(from_de_error(e)), - } - }) - .unwrap_or_default(); - // Let the CLI options take precedent if any provided - // TODO: this logic is bad, I really need to know if the CLI - // stuff was present and whether the stuff was present in the config - // but instead I just see two different states that need to be merged. - let cli_config = self - .app_config - .read() - .map_err(|e| anyhow!("failed to acquire read lock on config: {e}"))? - .clone(); - let mut new_app_config = saved_config; - if cli_config.recent_layout.is_some() { - new_app_config.recent_layout = cli_config.recent_layout; - } - if cli_config.recent_splits.is_some() { - new_app_config.recent_splits = cli_config.recent_splits; - } - if cli_config.recent_autosplitter.is_some() { - new_app_config.recent_autosplitter = cli_config.recent_autosplitter; - } - if cli_config.use_autosplitter.is_some() { - new_app_config.use_autosplitter = cli_config.use_autosplitter; - } - if cli_config.frame_rate.is_some() { - new_app_config.frame_rate = cli_config.frame_rate; - } - if cli_config.polling_rate.is_some() { - new_app_config.polling_rate = cli_config.polling_rate; - } - if cli_config.reset_timer_on_game_reset.is_some() { - new_app_config.reset_timer_on_game_reset = cli_config.reset_timer_on_game_reset; - } - if cli_config.reset_game_on_timer_reset.is_some() { - new_app_config.reset_game_on_timer_reset = cli_config.reset_game_on_timer_reset; - } - if cli_config.global_hotkeys.is_some() { - new_app_config.global_hotkeys = cli_config.global_hotkeys; - } - *self - .app_config - .write() - .map_err(|e| anyhow!("failed to acquire write lock on config: {e}"))? = - new_app_config; - Ok(()) - }); - } - pub fn process_app_config(&mut self, ctx: &egui::Context) { use anyhow::Context; let mut queue = vec![]; @@ -695,6 +627,96 @@ impl LiveSplitCoreRenderer { println!("registered"); Ok(()) } + + pub fn nwa_auto_splitter_settings_editor(&mut self, ctx: &egui::Context) { + if self + .show_nwa_autosplitter_settings_dialog + .load(Ordering::Relaxed) + { + let show_deferred_viewport = self.show_nwa_autosplitter_settings_dialog.clone(); + let mut _game = self.game; + let mut _adr = self.address.write().clone(); + let mut _port = *self.port.write(); + + ctx.show_viewport_deferred( + egui::ViewportId::from_hash_of("NWA_deferred_viewport"), + egui::ViewportBuilder::default() + .with_title("NWA AutoSplitter Settings Editor") + .with_inner_size([200.0, 500.0]), + move |ctx, class| { + assert!( + class == egui::ViewportClass::Deferred, + "This egui backend doesn't support multiple viewports" + ); + // TODO: Fix this. It's not updating the value; probably move this into config + egui::CentralPanel::default().show(ctx, |ui| { + let mut game = _game; + let mut adr = _adr.clone(); + let port = _port; + ui.label("NWA address"); + ui.text_edit_singleline(&mut adr); + ui.label("NWA port"); + ui.text_edit_singleline(&mut port.to_string()); + ui.label("NWA Game"); + egui::ComboBox::from_id_salt("Game") + .selected_text(format!("{:?}", game)) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show_ui(ui, |ui| { + fill_drop_down(ui, &mut game); + }); + }); + + if ctx.input(|i| i.viewport().close_requested()) { + // Tell parent to close us. + show_deferred_viewport.store(false, Ordering::Relaxed); + } + }, + ); + } + } + + pub fn auto_splitter_settings_editor(&mut self, ctx: &egui::Context) { + if self + .show_edit_autosplitter_settings_dialog + .load(Ordering::Relaxed) + { + let show_deferred_viewport = self.show_edit_autosplitter_settings_dialog.clone(); + let a_settings = self.settings.clone(); + + ctx.show_viewport_deferred( + egui::ViewportId::from_hash_of("deferred_viewport"), + egui::ViewportBuilder::default() + .with_title("AutoSplitter Settings Editor") + .with_inner_size([200.0, 500.0]), + move |ctx, class| { + assert!( + class == egui::ViewportClass::Deferred, + "This egui backend doesn't support multiple viewports" + ); + egui::CentralPanel::default().show(ctx, |_ui| { + let settings_editor = egui::containers::Window::new("Settings Editor"); + settings_editor + .open(&mut show_deferred_viewport.load(Ordering::Relaxed)) + .resizable(true) + .collapsible(false) + .hscroll(true) + .vscroll(true) + .show(ctx, |ui| { + ctx.move_to_top(ui.layer_id()); + let settings = a_settings.clone(); + let mut roots = settings.write().roots(); + show_children(&mut settings.write(), ui, ctx, &mut roots); + }); + }); + + if ctx.input(|i| i.viewport().close_requested()) { + // Tell parent to close us. + show_deferred_viewport.store(false, Ordering::Relaxed); + } + }, + ); + } + } } impl eframe::App for LiveSplitCoreRenderer { @@ -721,7 +743,7 @@ impl eframe::App for LiveSplitCoreRenderer { self.is_exiting = true; self.confirm_save(frame.gl().expect("No GL context")) .unwrap(); - self.save_app_config(); + self.app_config.read().unwrap().save_app_config(); // aquire read lock then save app config } }); if self.can_exit { @@ -767,10 +789,7 @@ impl eframe::App for LiveSplitCoreRenderer { ); self.glow_canvas .paint_layer(ctx, egui::LayerId::background(), viewport); - //self.glow_canvas.paint_immediate(frame.gl().unwrap(), viewport); - let settings_editor = egui::containers::Window::new("Settings Editor"); egui::Area::new("livesplit".into()) - .enabled(!self.show_settings_editor) .movable(false) .show(ctx, |ui| { ui.set_width(ctx.input(|i| i.screen_rect.width())); @@ -847,18 +866,32 @@ impl eframe::App for LiveSplitCoreRenderer { } }); ui.menu_button("Autosplitter", |ui| { - if ui.button("Configure").clicked() { - self.show_settings_editor = true; - ui.close_menu(); - } - if ui.button("Load Configuration").clicked() { - ui.close_menu(); - self.open_autosplitter_dialog(&document_dir).unwrap(); - } - if ui.button("Save Configuration").clicked() { - ui.close_menu(); - self.save_autosplitter_dialog(&document_dir).unwrap(); - } + ui.menu_button("NWA", |ui| { + if ui.button("Configure").clicked() { + let show_deferred_viewport = true; + self.show_nwa_autosplitter_settings_dialog + .store(show_deferred_viewport, Ordering::Relaxed); + ui.close_menu(); + } + }); + ui.menu_button("QUSB2SNES", |ui| { + ui.menu_button("Super Metroid", |ui| { + if ui.button("Configure").clicked() { + let show_deferred_viewport = true; + self.show_edit_autosplitter_settings_dialog + .store(show_deferred_viewport, Ordering::Relaxed); + ui.close_menu(); + } + if ui.button("Load Configuration").clicked() { + ui.close_menu(); + self.open_autosplitter_dialog(&document_dir).unwrap(); + } + if ui.button("Save Configuration").clicked() { + ui.close_menu(); + self.save_autosplitter_dialog(&document_dir).unwrap(); + } + }); + }); }); ui.separator(); ui.add(egui::widgets::Label::new(format!( @@ -870,18 +903,10 @@ impl eframe::App for LiveSplitCoreRenderer { ctx.send_viewport_cmd(egui::viewport::ViewportCommand::Close) } }); - settings_editor - .open(&mut self.show_settings_editor) - .resizable(true) - .collapsible(false) - .hscroll(true) - .vscroll(true) - .show(ctx, |ui| { - ctx.move_to_top(ui.layer_id()); - let mut settings = self.settings.write(); - let mut roots = settings.roots(); - show_children(&mut settings, ui, ctx, &mut roots); - }); + + self.auto_splitter_settings_editor(ctx); + self.nwa_auto_splitter_settings_editor(ctx); + ctx.input(|i| { let scroll_delta = i.raw_scroll_delta; if scroll_delta.y > 0.0 { @@ -956,7 +981,7 @@ pub fn app_init( ) { let context = cc.egui_ctx.clone(); context.set_visuals(egui::Visuals::dark()); - app.load_app_config(); + // app.load_app_config(); if app.app_config.read().unwrap().global_hotkeys == Some(YesOrNo::Yes) { messagebox_on_error(|| app.enable_global_hotkeys()); } @@ -991,41 +1016,141 @@ pub fn app_init( // something equivalent to Arc> so it's safe // to clone them and pass the clone between threads. let timer = app.timer.clone(); - let settings = app.settings.clone(); let app_config = app.app_config.clone(); + // This thread deals with polling the SNES at a fixed rate. if app_config.read().unwrap().use_autosplitter == Some(YesOrNo::Yes) { - let _snes_polling_thread = ThreadBuilder::default() - .name("SNES Polling Thread".to_owned()) - // We could change this thread priority, but we probably - // should leave it at the default to make sure we get timely - // polling of SNES state - .spawn(move |_| { - loop { - let latency = Arc::new(RwLock::new((0.0, 0.0))); - print_on_error(|| -> anyhow::Result<()> { - let mut client = crate::usb2snes::SyncClient::connect() - .context("creating usb2snes connection")?; - client.set_name("annelid")?; - println!("Server version is {:?}", client.app_version()?); - let mut devices = client.list_device()?.to_vec(); - if devices.len() != 1 { - if devices.is_empty() { - Err(anyhow!("No devices present"))?; - } else { - Err(anyhow!("You need to select a device: {:#?}", devices))?; + if app_config.read().unwrap().autosplitter_type == Some(autosplitters::AType::QUSB2SNES) { + //QUSB2SNES stuff here + let settings = app.settings.clone(); + let _snes_polling_thread = ThreadBuilder::default() + .name("SNES Polling Thread".to_owned()) + // We could change this thread priority, but we probably + // should leave it at the default to make sure we get timely + // polling of SNES state + .spawn(move |_| { + loop { + let latency = Arc::new(RwLock::new((0.0, 0.0))); + print_on_error(|| -> anyhow::Result<()> { + let mut client = crate::usb2snes::SyncClient::connect() + .context("creating usb2snes connection")?; + client.set_name("annelid")?; + println!("Server version is {:?}", client.app_version()?); + let mut devices = client.list_device()?.to_vec(); + if devices.len() != 1 { + if devices.is_empty() { + Err(anyhow!("No devices present"))?; + } else { + Err(anyhow!("You need to select a device: {:#?}", devices))?; + } } - } - let device = devices.pop().ok_or(anyhow!("Device list was empty"))?; - println!("Using device: {device}"); - client.attach(&device)?; - println!("Connected."); - println!("{:#?}", client.info()?); - let mut autosplitter: Box = - Box::new(SuperMetroidAutoSplitter::new(settings.clone())); + let device = devices.pop().ok_or(anyhow!("Device list was empty"))?; + println!("Using device: {device}"); + client.attach(&device)?; + println!("Connected."); + println!("{:#?}", client.info()?); + + // TODO: make this generic as well based on user input or add game selector + let mut autosplitter: Box = + Box::new(SuperMetroidAutoSplitter::new(settings.clone())); + loop { + let summary = autosplitter.update(&mut client)?; + if summary.start { + timer + .write() + .map_err(|e| { + anyhow!("failed to acquire write lock on timer: {e}") + })? + .start() + .ok(); + } + if summary.reset + && app_config + .read() + .map_err(|e| { + anyhow!("failed to acquire read lock on config: {e}") + })? + .reset_timer_on_game_reset + == Some(YesOrNo::Yes) + { + timer + .write() + .map_err(|e| { + anyhow!("failed to acquire write lock on timer: {e}") + })? + .reset(true) + .ok(); + } + if summary.split { + if let Some(t) = autosplitter.gametime_to_seconds() { + timer + .write() + .map_err(|e| { + anyhow!( + "failed to acquire write lock on timer: {e}" + ) + })? + .set_game_time(t) + .ok(); + } + timer + .write() + .map_err(|e| { + anyhow!("failed to acquire write lock on timer: {e}") + })? + .split() + .ok(); + } + { + *latency.write() = + (summary.latency_average, summary.latency_stddev); + } + // If the timer gets reset, we need to make a fresh snes state + if let Ok(ThreadEvent::TimerReset) = sync_receiver.try_recv() { + autosplitter.reset_game_tracking(); + // Reset the snes + if app_config + .read() + .map_err(|e| { + anyhow!("failed to acquire read lock on config: {e}") + })? + .reset_game_on_timer_reset + == Some(YesOrNo::Yes) + { + client.reset()?; + } + } + std::thread::sleep(std::time::Duration::from_millis( + (1000.0 / polling_rate) as u64, + )); + } + }); + std::thread::sleep(std::time::Duration::from_millis(1000)); + } + }) + //TODO: fix this unwrap + .unwrap(); + } else if app_config.read().unwrap().autosplitter_type == Some(autosplitters::AType::NWA) { + //NWA stuff here + let game = app.game; + let address = app.address.read().clone(); + let port = *app.port.read(); + let _nwa_polling_thread = ThreadBuilder::default() + .name("NWA Polling Thread".to_owned()) + .spawn(move |_| loop { + let mut client = nwaobject(game, app_config.clone(), &address, port); + + print_on_error(|| -> anyhow::Result<()> { + client.emu_info(); + client.emu_game_info(); + client.emu_status(); + client.client_id(); + client.core_info(); + client.core_memories(); loop { - let summary = autosplitter.update(&mut client)?; - if summary.start { + // println!("{game:#?}"); + let auto_split_status = client.update().unwrap(); + if auto_split_status.start { timer .write() .map_err(|e| { @@ -1034,15 +1159,7 @@ pub fn app_init( .start() .ok(); } - if summary.reset - && app_config - .read() - .map_err(|e| { - anyhow!("failed to acquire read lock on config: {e}") - })? - .reset_timer_on_game_reset - == Some(YesOrNo::Yes) - { + if auto_split_status.reset { timer .write() .map_err(|e| { @@ -1051,16 +1168,7 @@ pub fn app_init( .reset(true) .ok(); } - if summary.split { - if let Some(t) = autosplitter.gametime_to_seconds() { - timer - .write() - .map_err(|e| { - anyhow!("failed to acquire write lock on timer: {e}") - })? - .set_game_time(t) - .ok(); - } + if auto_split_status.split { timer .write() .map_err(|e| { @@ -1069,34 +1177,66 @@ pub fn app_init( .split() .ok(); } - { - *latency.write() = - (summary.latency_average, summary.latency_stddev); - } - // If the timer gets reset, we need to make a fresh snes state - if let Ok(ThreadEvent::TimerReset) = sync_receiver.try_recv() { - autosplitter.reset_game_tracking(); - //Reset the snes - if app_config - .read() - .map_err(|e| { - anyhow!("failed to acquire read lock on config: {e}") - })? - .reset_game_on_timer_reset - == Some(YesOrNo::Yes) - { - client.reset()?; - } - } + std::thread::sleep(std::time::Duration::from_millis( (1000.0 / polling_rate) as u64, )); } }); std::thread::sleep(std::time::Duration::from_millis(1000)); - } - }) - //TODO: fix this unwrap - .unwrap(); + }) + //TODO: fix this unwrap + .unwrap(); + } else if app_config.read().unwrap().autosplitter_type == Some(autosplitters::AType::ASL) { + //TODO: unable to configure runtime + + // let test = livesplit_auto_splitting::Runtime::new(module, timer, settings_store); + // Livesplit autosplitter support + // use livesplit_auto_splitting::*; + // let test = ; + // let test = livesplit_auto_splitting::Timer; + // let module = livesplit_auto_splitting::Runtime:: + // livesplit_auto_splitting::Runtime::new(module, timer, settings_store) + // let x = livesplit_auto_splitting::Runtime::new(module, timer.write().unwrap().deref(), settings_store); + } else if app_config.read().unwrap().autosplitter_type == Some(autosplitters::AType::CUSTOM) + { + // TODO: process isn't consistently gotten + // TODO: reading crashes with either bad address as root or permission denied as user + // This is also linux only + + // use process_memory::*; + // use sysinfo::*; + // let mut x = 0_u64; + // let x = sysinfo::Pid::from(17696).as_u32(); + // let s = System::new_all(); + // for (pid, process) in s.processes() { + // println!("{} {:?}", pid, process.name()); + // } + // let count = s.processes().clone().("retroarch"); + // let count = s.processes_by_exact_name(OsStr::new("retroarch")).count(); + // let p = s.processes_by_exact_name(OsStr::new("retroarch")); + // if count == 2 { + // x = p.last().unwrap().pid().as_u32(); + // } + // println!("{x:?}"); + + // let arch = process_memory::Architecture::from_native(); + // let process_handle = process_memory::ProcessHandle::try_into_process_handle(&( + // x.try_into().unwrap(), + // arch, + // )) + // .unwrap(); + // let mut member = DataMember::::new_offset(process_handle, vec![0x10]); + // member.set_offset(vec![0x10]); + + // The memory offset can now be correctly calculated: + // called `Result::unwrap()` on an `Err` value: Os { code: 1, kind: PermissionDenied, message: "Operation not permitted" } + // println!( + // "Target memory location: {}", + // member.clone().get_offset().unwrap() + // ); + // The memory offset can now be used to retrieve and modify values: + // println!("Current value: {}", unsafe { member.read().unwrap() }); + } } } diff --git a/src/main.rs b/src/main.rs index 8da7ffb..d82936d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,19 +5,21 @@ pub mod autosplitters; pub mod config; pub mod hotkey; pub mod livesplit_renderer; +pub mod nwa; pub mod routes; pub mod usb2snes; pub mod utils; pub mod widget; -use autosplitters::supermetroid::Settings; +// use autosplitters::supermetroid::Settings; use clap::Parser; use eframe::egui; use livesplit_core::layout::{ComponentSettings, LayoutSettings}; use livesplit_core::{Layout, Run, Segment, Timer}; -use parking_lot::RwLock; +// use parking_lot::RwLock; +use std::env; use std::error::Error; -use std::sync::Arc; +// use std::sync::Arc; use config::app_config::*; use livesplit_renderer::*; @@ -54,44 +56,35 @@ fn customize_timer(timer: &mut livesplit_core::component::timer::Settings) { } fn main() -> std::result::Result<(), Box> { - let cli_config = AppConfig::parse(); - let settings = Settings::new(); - let settings = Arc::new(RwLock::new(settings)); - let mut run = Run::default(); - run.push_segment(Segment::new("")); + let mut config = AppConfig::load_app_config(AppConfig::new()); // load saved config into default object + AppConfig::update_from(&mut config, env::args()); // reads command line options into app config object + + // let settings = Settings::new(); // setttings for SM autosplitter + // let settings = Arc::new(RwLock::new(settings)); // creates a RW pointer to the autosplitter settings + + let mut run = Run::default(); // creates a default livesplit run object + run.push_segment(Segment::new("")); // push blank segment to run + + let timer = Timer::new(run) // create timer object + .expect("Run with at least one segment provided") // error message + .into_shared(); // makes timer sharable across threads - let timer = Timer::new(run) - .expect("Run with at least one segment provided") - .into_shared(); let options = eframe::NativeOptions { renderer: eframe::Renderer::Glow, viewport: egui::viewport::ViewportBuilder { ..Default::default() }, ..eframe::NativeOptions::default() - }; - let layout_settings = Layout::default_layout().settings(); - //customize_layout(&mut layout_settings); - let layout = Layout::from_settings(layout_settings); + }; // create egui display options - use std::sync::mpsc::sync_channel; - let (sync_sender, sync_receiver) = sync_channel(1); - - let project_dirs = directories::ProjectDirs::from("", "", "annelid") - .ok_or("Unable to computer configuration directory")?; - println!("project_dirs = {project_dirs:#?}"); + let layout_settings = Layout::default_layout().settings(); // create default layout settings + //customize_layout(&mut layout_settings); + let layout = Layout::from_settings(layout_settings); // create default layout - let preference_dir = project_dirs.preference_dir(); - std::fs::create_dir_all(preference_dir)?; + use std::sync::mpsc::sync_channel; + let (sync_sender, sync_receiver) = sync_channel(1); // create thread - let mut app = LiveSplitCoreRenderer::new( - timer, - layout, - settings, - sync_sender, - project_dirs, - cli_config, - ); + let mut app = LiveSplitCoreRenderer::new(timer, layout, sync_sender, config); // create livesplit-core renderer object eframe::run_native( "Annelid", @@ -99,7 +92,7 @@ fn main() -> std::result::Result<(), Box> { Box::new(move |cc| { livesplit_renderer::app_init(&mut app, sync_receiver, cc); Ok(Box::new(app)) - }), + }), // initialize livesplitrender and load into egui render box )?; Ok(()) } diff --git a/src/nwa.rs b/src/nwa.rs new file mode 100644 index 0000000..776cde6 --- /dev/null +++ b/src/nwa.rs @@ -0,0 +1,214 @@ +// pub mod nwa { + +use std::collections::HashMap; +use std::fmt::Debug; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::{Shutdown, SocketAddr, TcpStream, ToSocketAddrs}; +//use std::ptr::read; +use std::time::Duration; + +#[derive(Debug, PartialEq)] +pub enum ErrorKind { + InvalidError, + InvalidCommand, + InvalidArgument, + NotAllowed, + ProtocolError, +} +#[derive(Debug)] +pub struct NWAError { + pub kind: ErrorKind, + pub reason: String, +} + +#[derive(Debug)] +pub enum AsciiReply { + Ok, + Hash(HashMap), + ListHash(Vec>), +} +#[derive(Debug)] +pub enum EmulatorReply { + Ascii(AsciiReply), + Error(NWAError), + Binary(Vec), +} + +#[derive(Debug)] +pub struct NWASyncClient { + pub connection: TcpStream, + pub port: u32, + pub addr: SocketAddr, +} + +impl NWASyncClient { + pub fn connect(ip: &str, port: u32) -> Result { + let addr: Vec<_> = format!("{ip}:{port}") + .to_socket_addrs() + .expect("Can't resolve address") + .collect(); + //println!("{:?}", addr); + let co = TcpStream::connect_timeout(&addr[0], Duration::from_millis(1000))?; + Ok(NWASyncClient { + connection: co, + port, + addr: addr[0], + }) + } + + pub fn get_reply(&mut self) -> Result { + let mut read_stream = BufReader::new(self.connection.try_clone().unwrap()); + let mut first_byte = [0_u8; 1]; + if read_stream.read(&mut first_byte)? == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::ConnectionAborted, + "Read 0 byte", + )); + } + let first_byte = first_byte[0]; + + // Ascii + if first_byte == b'\n' { + let mut map: HashMap = HashMap::new(); + let mut line: Vec = vec![]; + loop { + line.clear(); + + let rep = read_stream.read_until(b'\n', &mut line)?; + //println!("{:?}", String::from_utf8(line.clone())); + if line[0] == b'\n' && map.is_empty() { + return Ok(EmulatorReply::Ascii(AsciiReply::Ok)); + } + if line[0] == b'\n' { + break; + } + if rep == 0 { + break; + } + let mut key = [0_u8; 100]; + let mut value = [0_u8; 1024]; + let mut cpt = 0; + while line[cpt] != b':' && line[cpt] != b'\n' { + key[cpt] = line[cpt]; + cpt += 1; + } + let end_key = cpt; + // Should have stopped on : + if line[cpt] == b'\n' { + return Err(std::io::Error::other("Mal formed reply")); + } + cpt += 1; + let offset = cpt; + while line[cpt] != b'\n' { + value[cpt - offset] = line[cpt]; + cpt += 1; + } + let end_value = cpt - offset; + map.insert( + String::from_utf8_lossy(&key[0..end_key]).to_string(), + String::from_utf8_lossy(&value[0..end_value]).to_string(), + ); + } + if map.contains_key("error") { + if let Some(reason) = map.get("reason") { + let mkind: ErrorKind = match map.get("error").unwrap().as_str() { + "protocol_error" => ErrorKind::ProtocolError, + "invalid_command" => ErrorKind::InvalidCommand, + "invalid_argument" => ErrorKind::InvalidArgument, + "not_allowed" => ErrorKind::NotAllowed, + _ => ErrorKind::InvalidError, + }; + return Ok(EmulatorReply::Error(NWAError { + kind: mkind, + reason: reason.to_string(), + })); + } else { + return Ok(EmulatorReply::Error(NWAError { + kind: ErrorKind::InvalidError, + reason: String::from("Invalid reason"), + })); + } + } + return Ok(EmulatorReply::Ascii(AsciiReply::Hash(map))); + } + if first_byte == 0 { + let mut header = vec![0; 4]; + let _r_size = read_stream.read(&mut header)?; + // println!(); + //println!("Reading {:}", r_size); + //println!("Header : {:?}", header); + let header = header; + let mut size: u32; + size = (header[0] as u32) << 24; + size += (header[1] as u32) << 16; + size += (header[2] as u32) << 8; + size += header[3] as u32; + //println!("Size : {:}", size); + let msize = size as usize; + let mut data: Vec = vec![0; msize]; + //println!("Size : {:}", size); + read_stream.read_exact(&mut data)?; + //println!("Size : {:}", size); + return Ok(EmulatorReply::Binary(data)); + } + Err(std::io::Error::other("Invalid reply")) + } + + pub fn execute_command( + &mut self, + cmd: &str, + arg_string: Option<&str>, + ) -> Result { + if arg_string.is_none() { + self.connection.write_all(format!("{cmd}\n").as_bytes())?; + } else { + self.connection + .write_all(format!("{} {}\n", cmd, arg_string.unwrap()).as_bytes())?; + } + self.get_reply() + } + + pub fn execute_raw_command(&mut self, cmd: &str, arg_string: Option<&str>) { + if arg_string.is_none() { + // TODO: handle the Err + let _ = self.connection.write_all(format!("{cmd}\n").as_bytes()); + } else { + // TODO: handle the Err + let _ = self + .connection + .write_all(format!("{} {}\n", cmd, arg_string.unwrap()).as_bytes()); + } + } + + pub fn send_data(&mut self, data: Vec) { + let mut buf: Vec = vec![0; 5]; + let size = data.len(); + buf[0] = 0; + buf[1] = ((size >> 24) & 0xFF) as u8; + buf[2] = ((size >> 16) & 0xFF) as u8; + buf[3] = ((size >> 8) & 0xFF) as u8; + buf[4] = (size & 0xFF) as u8; + // TODO: handle the Err + let _ = self.connection.write_all(&buf); + // TODO: handle the Err + let _ = self.connection.write_all(&data); + } + pub fn is_connected(&mut self) -> bool { + let mut buf = vec![0; 0]; + if let Ok(_usize) = self.connection.peek(&mut buf) { + return true; + } + false + } + + pub fn close(&mut self) { + // TODO: handle the Err + let _ = self.connection.shutdown(Shutdown::Both); + } + pub fn reconnected(&mut self) -> Result { + self.connection = TcpStream::connect_timeout(&self.addr, Duration::from_millis(1000))?; + Ok(true) + } +} + +// }