From ed672e6b9eab737090a8d37c87389861abb59ac6 Mon Sep 17 00:00:00 2001 From: Sutekh <101658846+SutekhVRC@users.noreply.github.com> Date: Sat, 3 Feb 2024 15:29:31 -0800 Subject: [PATCH] VibeCheck 0.4.0 merge - Toy support / Input processors / Multi param / etc (#54) * SPS processing * strum enum string * clippy * dedent handling, create inline functions * Fix early toy thread exit bug * Rename OSC MSG processor routine * Progress SPS Input Processor * SPS logic for length calculation and feature level calculation. * Mapping module * simplify some logic * Length calculation and debugging/testing * Sps processing UI (#52) * move input processor out of advanced * change tooltip to enable/disable app * change idle to idle speed * fix feature option alignment * normalize osc_parameter for backend * use key for featureform instead of useEffect * Length calculation * Random cleanup * Implement SPS Touch support * Fix rate level slider * Basic TPS processing * use const object for enum * add debounce on osc_param input * Version 0.4.0 Beta * SPS Touch/Frot/Legacy support * Add check in case of possible OOB * fix spacing, add inset around config/features * fix as const * re-fix rate level slider * Advanced config options (#53) * refactor featureform into context, modular components * add advanced config options * refactor core events into context * add advanced option hide/show * scrollbar visible on hover parent * Input processor tooltip * use tooltip enum, add wiki links * show level tweaks only when mode is selected * slider gutters * Sutekh likes binary oops --------- Co-authored-by: SutekhVRC Co-authored-by: TuTiDore Co-authored-by: TuTiDore <113271459+TuTiDore@users.noreply.github.com> --- src-tauri/Cargo.lock | 6 +- src-tauri/Cargo.toml | 4 +- .../{FeSocialLink.ts => FeBrowserLink.ts} | 2 +- src-tauri/bindings/FeVibeCheckConfig.ts | 2 +- src-tauri/src/frontend/frontend_native.rs | 22 +- src-tauri/src/frontend/frontend_types.rs | 10 +- src-tauri/src/main.rs | 43 +- src-tauri/src/osc/logic.rs | 29 +- src-tauri/src/osc_api/mod.rs | 108 +- src-tauri/src/osc_api/osc_api.rs | 24 +- src-tauri/src/toy_handling/handling.rs | 941 +++++++++--------- .../penetration_systems/mod.rs | 4 +- .../penetration_systems/sps/mapping.rs | 429 ++++++++ .../penetration_systems/sps/mod.rs | 144 ++- .../penetration_systems/tps/mod.rs | 22 +- src-tauri/src/toy_handling/mod.rs | 34 +- src-tauri/src/toy_handling/toyops.rs | 42 +- src-tauri/src/util/bluetooth.rs | 28 +- src-tauri/src/util/net.rs | 10 +- src-tauri/src/vcore/config.rs | 30 +- src-tauri/src/vcore/core.rs | 41 +- src-tauri/tauri.conf.json | 6 +- src/App.css | 6 +- src/App.tsx | 25 +- src/components/ExternalLogo.tsx | 17 +- src/components/FourPanel.tsx | 8 +- src/components/FourPanelContainer.tsx | 2 +- .../CoreEvents.tsx} | 86 +- src/data/constants.ts | 137 ++- src/features/Config.tsx | 48 +- src/features/FeatureForm.tsx | 755 ++++++++------ src/features/Toy.tsx | 30 +- src/features/ToySettings.tsx | 13 +- src/hooks/useSimulate.tsx | 4 +- src/hooks/useToys.tsx | 10 +- src/hooks/useVersion.tsx | 4 +- src/layout/Slider.tsx | 2 +- src/layout/Tooltip.tsx | 33 +- src/main.tsx | 5 +- src/utils/index.ts | 2 + 40 files changed, 1979 insertions(+), 1189 deletions(-) rename src-tauri/bindings/{FeSocialLink.ts => FeBrowserLink.ts} (52%) create mode 100644 src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mapping.rs rename src/{hooks/useCoreEvents.tsx => context/CoreEvents.tsx} (60%) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 06843ac..e0e3da8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4429,6 +4429,9 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -5352,7 +5355,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vibecheck" -version = "0.3.2" +version = "0.4.0" dependencies = [ "btleplug 0.10.5", "buttplug", @@ -5369,6 +5372,7 @@ dependencies = [ "rosc", "serde", "serde_json", + "strum", "sysinfo", "tauri", "tauri-build", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0f4d7c4..8f30a2c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vibecheck" -version = "0.3.2" +version = "0.4.0" edition = "2021" authors = ["SutekhVRC"] rust-version = "1.57" @@ -33,7 +33,7 @@ env_logger = "0.10.0" open = "3.2.0" dyn-clone = "1.0.16" tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } - +strum = { version="0.25.0", features = ["derive"] } # Tauri dependencies tauri = { version = "1.5.2", features = ["api-all", "system-tray", "updater"] } [features] diff --git a/src-tauri/bindings/FeSocialLink.ts b/src-tauri/bindings/FeBrowserLink.ts similarity index 52% rename from src-tauri/bindings/FeSocialLink.ts rename to src-tauri/bindings/FeBrowserLink.ts index 60df2af..d595e28 100644 --- a/src-tauri/bindings/FeSocialLink.ts +++ b/src-tauri/bindings/FeBrowserLink.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type FeSocialLink = "Github" | "VRChatGroup" | "Discord"; \ No newline at end of file +export type FeBrowserLink = "Github" | "VRChatGroup" | "Discord" | "ToyOptions" | "FeatureOptions"; \ No newline at end of file diff --git a/src-tauri/bindings/FeVibeCheckConfig.ts b/src-tauri/bindings/FeVibeCheckConfig.ts index fdc6395..b75246d 100644 --- a/src-tauri/bindings/FeVibeCheckConfig.ts +++ b/src-tauri/bindings/FeVibeCheckConfig.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { FeOSCNetworking } from "./FeOSCNetworking"; -export interface FeVibeCheckConfig { networking: FeOSCNetworking, scan_on_disconnect: boolean, minimize_on_exit: boolean, desktop_notifications: boolean, lc_override: string | null, } \ No newline at end of file +export interface FeVibeCheckConfig { networking: FeOSCNetworking, scan_on_disconnect: boolean, minimize_on_exit: boolean, desktop_notifications: boolean, lc_override: string | null, show_toy_advanced: boolean, show_feature_advanced: boolean, } \ No newline at end of file diff --git a/src-tauri/src/frontend/frontend_native.rs b/src-tauri/src/frontend/frontend_native.rs index 6fdfa3f..b039be4 100644 --- a/src-tauri/src/frontend/frontend_native.rs +++ b/src-tauri/src/frontend/frontend_native.rs @@ -7,7 +7,7 @@ use crate::{ config::toy::VCToyConfig, frontend::{ frontend_types::{ - FeSocialLink, FeToyAlter, FeToyEvent, FeVCFeatureType, FeVCToy, FeVibeCheckConfig, + FeBrowserLink, FeToyAlter, FeToyEvent, FeVCFeatureType, FeVCToy, FeVibeCheckConfig, }, FromFrontend, ToFrontend, }, @@ -27,7 +27,7 @@ use tauri::Manager; */ #[tauri::command] pub fn vibecheck_version(app_handle: tauri::AppHandle) -> String { - format!("{} windows", app_handle.package_info().version) + format!("{}-beta windows", app_handle.package_info().version) } /* @@ -246,17 +246,27 @@ pub fn alter_toy( * Opens the social link specified */ #[tauri::command(async)] -pub fn open_default_browser(link: FeSocialLink) { +pub fn open_default_browser(link: FeBrowserLink) { match link { - FeSocialLink::Discord => { + FeBrowserLink::Discord => { let _ = open::that("https://discord.gg/g6kUFtMtpw"); } - FeSocialLink::Github => { + FeBrowserLink::Github => { let _ = open::that("https://github.com/SutekhVRC/VibeCheck"); } - FeSocialLink::VRChatGroup => { + FeBrowserLink::VRChatGroup => { let _ = open::that("https://vrc.group/VIBE.9503"); } + FeBrowserLink::ToyOptions => { + let _ = open::that( + "https://github.com/SutekhVRC/VibeCheck/wiki/Toy-Options-List#toy-options", + ); + } + FeBrowserLink::FeatureOptions => { + let _ = open::that( + "https://github.com/SutekhVRC/VibeCheck/wiki/Toy-Options-List#toy-feature-options", + ); + } }; } diff --git a/src-tauri/src/frontend/frontend_types.rs b/src-tauri/src/frontend/frontend_types.rs index b9fc0c0..b3d2340 100644 --- a/src-tauri/src/frontend/frontend_types.rs +++ b/src-tauri/src/frontend/frontend_types.rs @@ -20,6 +20,8 @@ pub struct FeVibeCheckConfig { pub minimize_on_exit: bool, pub desktop_notifications: bool, pub lc_override: Option, + pub show_toy_advanced: bool, + pub show_feature_advanced: bool, } #[derive(Deserialize, Serialize, Debug, Clone, TS)] @@ -64,10 +66,12 @@ pub enum FeCoreEvent { #[derive(Deserialize, Clone, TS)] #[ts(export)] -pub enum FeSocialLink { +pub enum FeBrowserLink { Github, VRChatGroup, Discord, + ToyOptions, + FeatureOptions, } #[derive(Serialize, Deserialize, Clone, TS, Debug)] @@ -229,8 +233,4 @@ impl PartialEq for FeVCFeatureType { fn eq(&self, other: &VCFeatureType) -> bool { *self as u32 == *other as u32 } - - fn ne(&self, other: &VCFeatureType) -> bool { - !self.eq(other) - } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a179cf9..5626df7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -133,28 +133,27 @@ fn main() { app.run(|_app_handle, event| { match event { - tauri::RunEvent::WindowEvent { label, event, .. } => { - match event { - tauri::WindowEvent::CloseRequested { api, .. } => { - let minimize_on_exit = { - _app_handle - .state::() - .0 - .lock() - .config - .minimize_on_exit - }; - - if minimize_on_exit { - let window = _app_handle.get_window(&label).unwrap(); - trace!("Closing window: {}", window.label()); - window.hide().unwrap(); - api.prevent_close(); - } else { - // Let exit - } - } - _ => {} + tauri::RunEvent::WindowEvent { + label, + event: tauri::WindowEvent::CloseRequested { api, .. }, + .. + } => { + let minimize_on_exit = { + _app_handle + .state::() + .0 + .lock() + .config + .minimize_on_exit + }; + + if minimize_on_exit { + let window = _app_handle.get_window(&label).unwrap(); + trace!("Closing window: {}", window.label()); + window.hide().unwrap(); + api.prevent_close(); + } else { + // Let exit } } tauri::RunEvent::ExitRequested { .. } => { diff --git a/src-tauri/src/osc/logic.rs b/src-tauri/src/osc/logic.rs index 549def7..0f16639 100644 --- a/src-tauri/src/osc/logic.rs +++ b/src-tauri/src/osc/logic.rs @@ -66,9 +66,7 @@ pub fn toy_input_routine( // Send address and arg to broadcast channel // Die when channel disconnects - if vibecheck_osc_api(&bind_sock, &app_handle, &toy_bcst_tx) { - continue; - } else { + if !vibecheck_osc_api(&bind_sock, &app_handle, &toy_bcst_tx) { return; } } @@ -111,7 +109,7 @@ pub async fn vc_disabled_osc_command_listen(app_handle: AppHandle, vc_config: OS } }; - if br <= 0 { + if br == 0 { continue; } else { let pkt = match rosc::decoder::decode_udp(&buf) { @@ -152,24 +150,13 @@ pub fn recv_osc_cmd(sock: &UdpSocket) -> Option { } }; - if br <= 0 { + if br == 0 { return None; - } else { - let pkt = match rosc::decoder::decode_udp(&buf) { - Ok(pkt) => pkt, - Err(_e) => { - return None; - } - }; - - match pkt.1 { - OscPacket::Message(msg) => { - return Some(msg); - } - _ => { - return None; - } - } + } + let pkt = rosc::decoder::decode_udp(&buf).ok()?; + match pkt.1 { + OscPacket::Message(msg) => Some(msg), + _ => None, } } diff --git a/src-tauri/src/osc_api/mod.rs b/src-tauri/src/osc_api/mod.rs index 3df5c58..f0a3803 100644 --- a/src-tauri/src/osc_api/mod.rs +++ b/src-tauri/src/osc_api/mod.rs @@ -12,69 +12,65 @@ impl APIProcessor { pub fn parse(mut endpoint: OscMessage, app_handle: &AppHandle) { let mut api_tokenize = endpoint .addr - .split("/") + .split('/') .map(|t| t.to_string()) .collect::>(); - api_tokenize.retain(|token| token.len() > 0); + api_tokenize.retain(|token| !token.is_empty()); debug!("[*] API tokenization: {:?}", api_tokenize); - if api_tokenize.len() == 5 { - if api_tokenize[2] == "vibecheck" && api_tokenize[3] == "api" { - if api_tokenize[4] == "state" { - // /avatar/parameters/vibecheck/api/state - if let Some(state_bool) = endpoint.args.pop().unwrap().bool() { - if !state_bool { - info!("State false: Sending Disable event"); - let _ = app_handle.emit_all( - "fe_core_event", - FeCoreEvent::State( - crate::frontend::frontend_types::FeStateEvent::Disable, - ), + if api_tokenize.len() == 5 + && api_tokenize[2] == "vibecheck" + && api_tokenize[3] == "api" + && api_tokenize[4] == "state" + { + // /avatar/parameters/vibecheck/api/state + let Some(false) = endpoint.args.pop().unwrap().bool() else { + return; + }; + info!("State false: Sending Disable event"); + let _ = app_handle.emit_all( + "fe_core_event", + FeCoreEvent::State(crate::frontend::frontend_types::FeStateEvent::Disable), + ); + } else if api_tokenize.len() == 7 + && api_tokenize[4] == "anatomy" + && api_tokenize[6] == "enabled" + { + // /avatar/parameters/vibecheck/api/anatomy/Anal/enabled + trace!("[*] Checking anatomy token: {}", api_tokenize[5]); + let anatomy = VCToyAnatomy::get_anatomy(&api_tokenize[5]); + let mut altered_toys = Vec::new(); + + if let Some(state_bool) = endpoint.args.pop().unwrap().bool() { + let vc_pointer = app_handle + .state::() + .0 + .clone(); + let mut vc_lock = vc_pointer.lock(); + + vc_lock + .core_toy_manager + .as_mut() + .unwrap() + .online_toys + .iter_mut() + .for_each(|toy| { + if toy.1.mutate_state_by_anatomy(&anatomy, state_bool) { + trace!( + "[*] Mutating feature state from anatomy for toy: {}", + toy.1.toy_name ); + altered_toys.push(toy.1.clone()); } - } - } + }); } - } else if api_tokenize.len() == 7 { - if api_tokenize[4] == "anatomy" && api_tokenize[6] == "enabled" { - // /avatar/parameters/vibecheck/api/anatomy/Anal/enabled - - trace!("[*] Checking anatomy token: {}", api_tokenize[5]); - - let anatomy = VCToyAnatomy::get_anatomy(&api_tokenize[5]); - let mut altered_toys = Vec::new(); - if let Some(state_bool) = endpoint.args.pop().unwrap().bool() { - let vc_pointer = app_handle - .state::() - .0 - .clone(); - let mut vc_lock = vc_pointer.lock(); - - vc_lock - .core_toy_manager - .as_mut() - .unwrap() - .online_toys - .iter_mut() - .for_each(|toy| { - if toy.1.mutate_state_by_anatomy(&anatomy, state_bool) { - trace!( - "[*] Mutating feature state from anatomy for toy: {}", - toy.1.toy_name - ); - altered_toys.push(toy.1.clone()); - } - }); - } - - altered_toys.iter().for_each(|toy| { - let _ = vcore::core::native_alter_toy( - app_handle.state::(), - app_handle.clone(), - toy.clone(), - ); - }); - } + altered_toys.iter().for_each(|toy| { + let _ = vcore::core::native_alter_toy( + app_handle.state::(), + app_handle.clone(), + toy.clone(), + ); + }); } } } diff --git a/src-tauri/src/osc_api/osc_api.rs b/src-tauri/src/osc_api/osc_api.rs index e68a82b..83c2f1f 100644 --- a/src-tauri/src/osc_api/osc_api.rs +++ b/src-tauri/src/osc_api/osc_api.rs @@ -11,7 +11,7 @@ pub fn vibecheck_osc_api( app_handle: &AppHandle, toy_bcst_tx: &BSender, ) -> bool { - match recv_osc_cmd(&bind_sock) { + match recv_osc_cmd(bind_sock) { Some(msg) => { // Stop toys on avatar change if msg.addr.starts_with("/avatar/change") { @@ -22,34 +22,36 @@ pub fn vibecheck_osc_api( .0 .clone(); let vc_lock = vc_pointer.lock(); - let _ = vc_lock + vc_lock .async_rt .block_on(async { vc_lock.bp_client.as_ref().unwrap().stop_all_devices().await }) .unwrap(); } - return true; + true } else if msg.addr.starts_with("/avatar/parameters/vibecheck/api/") { trace!("[*] VibeCheck API: {:?}", msg); - APIProcessor::parse(msg, &app_handle); - return true; + APIProcessor::parse(msg, app_handle); + true } else { // Not a vibecheck OSC command, broadcast to toys - - if let Err(_) = toy_bcst_tx.send(ToySig::OSCMsg(msg)) { + if toy_bcst_tx.send(ToySig::OSCMsg(msg)).is_err() { info!("BCST TX is disconnected. Shutting down toy input routine!"); - return false; // Shutting down handler_routine + // Shutting down handler_routine + false + } else { + true } - return true; } } None => { if toy_bcst_tx.receiver_count() == 0 { info!("BCST TX is disconnected (RECV C=0). Shutting down toy input routine!"); - return false; + false + } else { + true } - return true; } } } diff --git a/src-tauri/src/toy_handling/handling.rs b/src-tauri/src/toy_handling/handling.rs index 3bcd364..f2a02b2 100644 --- a/src-tauri/src/toy_handling/handling.rs +++ b/src-tauri/src/toy_handling/handling.rs @@ -1,3 +1,20 @@ +use crate::config::OSCNetworking; +use crate::frontend::frontend_types::FeCoreEvent; +use crate::frontend::frontend_types::FeScanEvent; +use crate::frontend::frontend_types::FeToyEvent; +use crate::frontend::frontend_types::FeVCToy; +use crate::frontend::ToFrontend; +use crate::osc::logic::toy_input_routine; +use crate::toy_handling::toy_manager::ToyManager; +use crate::toy_handling::toyops::LevelTweaks; +use crate::toy_handling::toyops::ToyParameter; +use crate::toy_handling::toyops::VCFeatureType; +use crate::toy_handling::toyops::{VCToy, VCToyFeatures}; +use crate::toy_handling::ToyPower; +use crate::toy_handling::ToySig; +use crate::vcore::core::ToyManagementEvent; +use crate::vcore::core::VibeCheckState; +use crate::{vcore::core::TmSig, vcore::core::ToyUpdate, vcore::core::VCError}; use buttplug::client::ButtplugClientDevice; use buttplug::client::ButtplugClientEvent; use buttplug::client::RotateCommand::RotateMap; @@ -6,49 +23,28 @@ use buttplug::core::message::ActuatorType; use futures::StreamExt; use futures_timer::Delay; use log::debug; +use log::{error as logerr, info, trace, warn}; use parking_lot::Mutex; - +use rosc::OscMessage; use rosc::OscType; -use tauri::AppHandle; -use tauri::Manager; - use std::collections::HashMap; -use tokio::sync::mpsc::UnboundedReceiver; - use std::sync::mpsc::Sender; use std::sync::Arc; use std::thread; use std::time::Duration; use std::time::Instant; +use tauri::api::notification::Notification; +use tauri::AppHandle; +use tauri::Manager; use tokio::runtime::Runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::UnboundedSender; use tokio::sync::{ self, broadcast::{Receiver as BReceiver, Sender as BSender}, }; use tokio::task::JoinHandle; -use crate::config::OSCNetworking; -use crate::frontend::frontend_types::FeCoreEvent; -use crate::frontend::frontend_types::FeScanEvent; -use crate::frontend::frontend_types::FeToyEvent; -use crate::frontend::frontend_types::FeVCToy; -use crate::frontend::ToFrontend; - -use crate::osc::logic::toy_input_routine; -use crate::toy_handling::toy_manager::ToyManager; -use crate::toy_handling::toyops::LevelTweaks; -use crate::toy_handling::toyops::ToyParameter; -use crate::toy_handling::toyops::VCFeatureType; -use crate::toy_handling::toyops::{VCToy, VCToyFeatures}; -use crate::toy_handling::ToyPower; -use crate::toy_handling::ToySig; -use crate::vcore::core::ToyManagementEvent; -use crate::vcore::core::VibeCheckState; -use crate::{vcore::core::TmSig, vcore::core::ToyUpdate, vcore::core::VCError}; -use log::{error as logerr, info, trace, warn}; -use tauri::api::notification::Notification; -use tokio::sync::mpsc::UnboundedSender; - use super::toyops::ProcessingMode; use super::toyops::ProcessingModeValues; use super::toyops::RateProcessingValues; @@ -133,7 +129,7 @@ pub async fn client_event_handler( Err(e) => warn!("Toy config failed to load: {:?}", e), } - if let None = toy.config { + if toy.config.is_none() { // First time toy load toy.populate_toy_config(); let mut vc_lock = vibecheck_state_pointer.lock(); @@ -223,7 +219,7 @@ pub async fn client_event_handler( if vc_lock.config.desktop_notifications { let _ = Notification::new(identifier.clone()) .title("Toy Disconnected") - .body(format!("{}", toy.toy_name).as_str()) + .body(toy.toy_name.to_string()) .show(); } } @@ -232,7 +228,7 @@ pub async fn client_event_handler( info!("Scan on disconnect is enabled.. Starting scan."); let vc_lock = vibecheck_state_pointer.lock(); if vc_lock.bp_client.is_some() && vc_lock.config.scan_on_disconnect { - let _ = vc_lock + vc_lock .async_rt .spawn(vc_lock.bp_client.as_ref().unwrap().start_scanning()); } @@ -337,10 +333,9 @@ fn parse_smoothing( } else { debug!("Setting float_level with smoothed float"); // Get Mean of all numbers in smoothing rate and then round to hundredths and cast as f64 - float_level = ((smooth_queue.iter().sum::() as f64 / smooth_queue.len() as f64 - * 100.0) + float_level = (smooth_queue.iter().sum::() / smooth_queue.len() as f64 * 100.0) .round() - / 100.0) as f64; + / 100.0; smooth_queue.clear(); smooth_queue.push(float_level); @@ -448,24 +443,23 @@ async fn mode_processor<'toy_parameter>( match input_type { ModeProcessorInputType::Float(f_input) => { // Input Processor & Float - return mode_processor_logic( + mode_processor_logic( ModeProcessorInputType::Float(f_input), processing_mode_values, feature_levels, flip_input, ) - .await; + .await } - ModeProcessorInputType::Boolean(b_input) => { // Input Processor & Boolean - return mode_processor_logic( + mode_processor_logic( ModeProcessorInputType::Boolean(b_input), processing_mode_values, feature_levels, flip_input, ) - .await; + .await } // Input Processor & Boolean } } @@ -474,24 +468,23 @@ async fn mode_processor<'toy_parameter>( match input_type { ModeProcessorInputType::Float(f_input) => { // Raw Input & Float - return mode_processor_logic( + mode_processor_logic( ModeProcessorInputType::Float(f_input), &mut toy_parameter.processing_mode_values, feature_levels, flip_input, ) - .await; + .await } - ModeProcessorInputType::Boolean(b_input) => { // Raw Input & Boolean - return mode_processor_logic( + mode_processor_logic( ModeProcessorInputType::Boolean(b_input), &mut toy_parameter.processing_mode_values, feature_levels, flip_input, ) - .await; + .await } // Raw Input & Boolean } } @@ -550,7 +543,7 @@ async fn mode_processor_logic( ProcessingModeValues::Rate(values) => { //trace!("parse_rate()"); // Need to set rate_timestamp when feature enabled - if let None = values.rate_timestamp { + if values.rate_timestamp.is_none() { values.rate_timestamp = Some(Instant::now()); } @@ -597,6 +590,7 @@ async fn mode_processor_logic( + Send toy updates like (battery updates) */ // Uses TME send and recv channel + pub async fn toy_management_handler( tme_send: UnboundedSender, mut tme_recv: UnboundedReceiver, @@ -616,257 +610,14 @@ pub async fn toy_management_handler( // Lock this to a user-set HZ value while dev.connected() { - //trace!("Toy recv loop start"); - match toy_bcst_rx.recv().await { - Ok(ts) => { - match ts { - ToySig::OSCMsg(mut msg) => { - // Parse OSC msgs to toys commands - //debug!("msg.addr = {} | msg.args = {:?}", msg.addr, msg.args); - /* - * Do Penetration System parsing first? - * Then parameter parsing? - * Mode processor is a function now so it can be used in both! - */ - - // msg args pop should go here - //let newest_msg_addr = msg.addr.clone(); - let newest_msg_val = msg.args.pop().unwrap(); - - /* - * Input mode processing - * Get all features with an enabled Input mode - * For each feature with a penetration system do processing for the current OSC input - * If get a value from processing check if the Input mode has a processing mode associated - * - */ - - // Get all features with an enabled input processor? - if let Some(input_processor_system_features) = - vc_toy_features.get_features_with_input_processors(&msg.addr) - { - match newest_msg_val { - OscType::Float(lvl) => { - for feature in input_processor_system_features { - let float_level = - ((lvl * 100.0).round() / 100.0) as f64; - // pen_system is checked for None in get_features_with_penetration_systems method. - // Give access to internal mode values here (input, internal_values) - if let Some(i_mode_processed_value) = feature - .penetration_system - .pen_system - .as_mut() - .unwrap() - .process( - msg.addr.as_str(), - ModeProcessorInputType::Float(float_level), - ) - { - // Send to mode processor if specified (Raw = no mode processing) - if let ProcessingMode::Raw = feature - .penetration_system - .pen_system_processing_mode - { - command_toy( - dev.clone(), - feature.feature_type, - i_mode_processed_value, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } else { - // If mode processor returns a value send to toy - if let Some(i) = mode_processor(ModeProcessorInput::InputProcessor((ModeProcessorInputType::Float(i_mode_processed_value), &mut feature.penetration_system.pen_system_processing_mode_values)), feature.feature_levels, feature.flip_input_float).await { - command_toy( - dev.clone(), - feature.feature_type, - i, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } - } - } - } - } - // Boolean can be supported in the process trait method - OscType::Bool(b) => { - for feature in input_processor_system_features { - // Boolean to float transformation here - if let Some(i_mode_processed_value) = feature - .penetration_system - .pen_system - .as_mut() - .unwrap() - .process( - msg.addr.as_str(), - ModeProcessorInputType::Boolean(b), - ) - { - // Send to mode processor if specified (Raw = no mode processing) - if let ProcessingMode::Raw = feature - .penetration_system - .pen_system_processing_mode - { - command_toy( - dev.clone(), - feature.feature_type, - i_mode_processed_value, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } else { - if let Some(i) = mode_processor( - ModeProcessorInput::InputProcessor(( - ModeProcessorInputType::Float(i_mode_processed_value), - &mut feature - .penetration_system - .pen_system_processing_mode_values, - )), - feature.feature_levels, - feature.flip_input_float - ) - .await - { - command_toy( - dev.clone(), - feature.feature_type, - i, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } - } - } - } - } - _ => (), - } // End match OscType for Input processors - } // End Input processing - - if let Some(features) = - vc_toy_features.get_features_from_param(&msg.addr) - { - match newest_msg_val { - OscType::Float(lvl) => { - // Clamp float accuracy to hundredths and cast as 64 bit float - let float_level = - ((lvl * 100.0).round() / 100.0) as f64; - //debug!("Received and cast float lvl: {:.5}", float_level); - - for feature in features { - // Get ToyParameter here - // We unwrap here because the call to get_features_from_param guarantees the parameter exists. - let mut toy_parameter = feature - .osc_parameters - .iter_mut() - .filter_map(|param| { - if param.parameter == msg.addr { - Some(param) - } else { - None - } - }) - .collect::>(); - - if let Some(first_toy_param) = - toy_parameter.first_mut() - { - if let Some(mode_processed_value) = - mode_processor( - ModeProcessorInput::RawInput( - ModeProcessorInputType::Float( - float_level, - ), - first_toy_param, - ), - feature.feature_levels, - feature.flip_input_float, - ) - .await - { - command_toy( - dev.clone(), - feature.feature_type, - mode_processed_value, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } - } // If no matching toy parameter skip feature - } - } - OscType::Bool(b) => { - info!("Got a Bool! {} = {}", msg.addr, b); - for feature in features { - // Get ToyParameter here - let mut toy_parameter = feature - .osc_parameters - .iter_mut() - .filter_map(|param| { - if param.parameter == msg.addr { - Some(param) - } else { - None - } - }) - .collect::>(); - - if let Some(first_toy_param) = - toy_parameter.first_mut() - { - if let Some(i) = mode_processor( - ModeProcessorInput::RawInput( - ModeProcessorInputType::Boolean(b), - first_toy_param, - ), - feature.feature_levels, - feature.flip_input_float, - ) - .await - { - command_toy( - dev.clone(), - feature.feature_type, - i, - feature.feature_index, - feature.flip_input_float, - feature.feature_levels, - ) - .await; - } - } - } - } - _ => {} // Skip parameter because unsuppported OSC type - } - } - } - ToySig::UpdateToy(toy) => { - match toy { - // Update feature map while toy running! - ToyUpdate::AlterToy(new_toy) => { - if new_toy.toy_id == dev.index() { - vc_toy_features = new_toy.parsed_toy_features; - info!("Altered toy: {}", new_toy.toy_id); - } - } - _ => {} // Remove and Add are handled internally from device connected state and management loop (listening) - } - } - } + let Ok(ts) = toy_bcst_rx.recv().await else { + continue; + }; + match ts { + ToySig::OSCMsg(mut msg) => { + parse_osc_message(&mut msg, dev.clone(), &mut vc_toy_features).await } - Err(_e) => {} + ToySig::UpdateToy(toy) => update_toy(toy, dev.clone(), &mut vc_toy_features), } } info!( @@ -881,211 +632,443 @@ pub async fn toy_management_handler( // Management loop loop { // Recv event (not listening) - match tme_recv.recv().await { - Some(event) => { - match event { - // Handle Toy Update Signals - ToyManagementEvent::Tu(tu) => match tu { + if let Some(event) = tme_recv.recv().await { + match event { + // Handle Toy Update Signals + ToyManagementEvent::Tu(tu) => match tu { + ToyUpdate::AddToy(toy) => { + core_toy_manager.online_toys.insert(toy.toy_id, toy); + } + ToyUpdate::RemoveToy(id) => { + core_toy_manager.online_toys.remove(&id); + } + ToyUpdate::AlterToy(toy) => { + core_toy_manager.online_toys.insert(toy.toy_id, toy); + } + }, + // Handle Management Signals + ToyManagementEvent::Sig(tm_sig) => { + match tm_sig { + TmSig::StartListening(osc_net) => { + vc_config = osc_net; + listening = true; + } + TmSig::StopListening => { + // Already not listening + info!("StopListening but not listening"); + } + TmSig::TMHReset => { + info!("TMHReset but not listening"); + } + _ => {} + } + } + } // Event handled + } + + if !listening { + continue; + } + + // This is a nested runtime maybe remove + // Would need to pass toy thread handles to VibeCheckState + let toy_async_rt = Runtime::new().unwrap(); + info!("Started listening!"); + // Recv events (listening) + // Create toy bcst channel + + // Toy threads + let mut running_toy_ths: HashMap> = HashMap::new(); + + // Broadcast channels for toy commands + let (toy_bcst_tx, _toy_bcst_rx): (BSender, BReceiver) = + sync::broadcast::channel(1024); + + // Create toy threads + for toy in &core_toy_manager.online_toys { + let f_run = f( + toy.1.device_handle.clone(), + toy_bcst_tx.subscribe(), + toy.1.parsed_toy_features.clone(), + ); + running_toy_ths.insert( + *toy.0, + toy_async_rt.spawn(async move { + f_run.await; + }), + ); + info!("Toy: {} started listening..", *toy.0); + } + + // Create OSC listener thread + let toy_bcst_tx_osc = toy_bcst_tx.clone(); + info!("Spawning OSC listener.."); + let vc_conf_clone = vc_config.clone(); + let tme_send_clone = tme_send.clone(); + let app_handle_clone = app_handle.clone(); + thread::spawn(move || { + toy_input_routine( + toy_bcst_tx_osc, + tme_send_clone, + app_handle_clone, + vc_conf_clone, + ) + }); + + loop { + // Recv event (listening) + let event = tme_recv.recv().await; + let Some(event) = event else { continue }; + match event { + // Handle Toy Update Signals + ToyManagementEvent::Tu(tu) => { + match tu { ToyUpdate::AddToy(toy) => { - core_toy_manager.online_toys.insert(toy.toy_id, toy); + core_toy_manager.online_toys.insert(toy.toy_id, toy.clone()); + let f_run = f( + toy.device_handle, + toy_bcst_tx.subscribe(), + toy.parsed_toy_features.clone(), + ); + running_toy_ths.insert( + toy.toy_id, + toy_async_rt.spawn(async move { + f_run.await; + }), + ); + info!("Toy: {} started listening..", toy.toy_id); } ToyUpdate::RemoveToy(id) => { - core_toy_manager.online_toys.remove(&id); + // OSC Listener thread will only die on StopListening event + if let Some(toy) = running_toy_ths.remove(&id) { + toy.abort(); + match toy.await { + Ok(()) => info!("Toy {} thread finished", id), + Err(e) => { + warn!("Toy {} thread failed to reach completion: {}", id, e) + } + } + info!("[TOY ID: {}] Stopped listening. (ToyUpdate::RemoveToy)", id); + running_toy_ths.remove(&id); + core_toy_manager.online_toys.remove(&id); + } } ToyUpdate::AlterToy(toy) => { + match toy_bcst_tx + .send(ToySig::UpdateToy(ToyUpdate::AlterToy(toy.clone()))) + { + Ok(receivers) => { + info!("Sent ToyUpdate broadcast to {} toys", receivers - 1) + } + Err(e) => { + logerr!("Failed to send UpdateToy: {}", e) + } + } core_toy_manager.online_toys.insert(toy.toy_id, toy); } - }, - // Handle Management Signals - ToyManagementEvent::Sig(tm_sig) => { - match tm_sig { - TmSig::StartListening(osc_net) => { - vc_config = osc_net; - listening = true; - } - TmSig::StopListening => { - // Already not listening - info!("StopListening but not listening"); + } + } + // Handle Management Signals + ToyManagementEvent::Sig(tm_sig) => { + match tm_sig { + TmSig::StartListening(osc_net) => { + vc_config = osc_net; + // Already listening + } + TmSig::StopListening => { + // Stop listening on every device and clean running thread hashmap + + for toy in &mut running_toy_ths { + toy.1.abort(); + match toy.1.await { + Ok(()) => { + info!("Toy {} thread finished", toy.0) + } + Err(e) => warn!( + "Toy {} thread failed to reach completion: {}", + toy.0, e + ), + } + info!("[TOY ID: {}] Stopped listening. (TMSIG)", toy.0); } - TmSig::TMHReset => { - info!("TMHReset but not listening"); + running_toy_ths.clear(); + drop(_toy_bcst_rx); // Causes OSC listener to die + toy_async_rt.shutdown_background(); + listening = false; + info!("Toys: {}", core_toy_manager.online_toys.len()); + break; //Stop Listening + } + TmSig::TMHReset => { + // Stop listening on every device and clean running thread hashmap + info!("TMHReset"); + + for toy in &mut running_toy_ths { + toy.1.abort(); + match toy.1.await { + Ok(()) => { + info!("Toy {} thread finished", toy.0) + } + Err(e) => warn!( + "Toy {} thread failed to reach completion: {}", + toy.0, e + ), + } + info!("[TOY ID: {}] Stopped listening. (TMSIG)", toy.0); } - _ => {} + running_toy_ths.clear(); + drop(_toy_bcst_rx); // Causes OSC listener to die + toy_async_rt.shutdown_background(); + listening = false; + info!("Toys: {}", core_toy_manager.online_toys.len()); + break; //Stop Listening } + _ => {} } } // Event handled } - None => {} } + } // Management loop +} - if listening { - // This is a nested runtime maybe remove - // Would need to pass toy thread handles to VibeCheckState - let toy_async_rt = Runtime::new().unwrap(); - info!("Started listening!"); - // Recv events (listening) - // Create toy bcst channel - - // Toy threads - let mut running_toy_ths: HashMap> = HashMap::new(); - - // Broadcast channels for toy commands - let (toy_bcst_tx, _toy_bcst_rx): (BSender, BReceiver) = - sync::broadcast::channel(1024); - - // Create toy threads - for toy in &core_toy_manager.online_toys { - let f_run = f( - toy.1.device_handle.clone(), - toy_bcst_tx.subscribe(), - toy.1.parsed_toy_features.clone(), - ); - running_toy_ths.insert( - *toy.0, - toy_async_rt.spawn(async move { - f_run.await; - }), - ); - info!("Toy: {} started listening..", *toy.0); +#[inline(always)] +async fn parse_osc_message( + msg: &mut OscMessage, + dev: Arc, + vc_toy_features: &mut VCToyFeatures, +) { + // Parse OSC msgs to toys commands + //debug!("msg.addr = {} | msg.args = {:?}", msg.addr, msg.args); + /* + * Do Penetration System parsing first? + * Then parameter parsing? + * Mode processor is a function now so it can be used in both! + */ + + // msg args pop should go here + //let newest_msg_addr = msg.addr.clone(); + let newest_msg_val = msg.args.pop().unwrap(); + + /* + * Input mode processing + * Get all features with an enabled Input mode + * For each feature with a penetration system do processing for the current OSC input + * If get a value from processing check if the Input mode has a processing mode associated + * + */ + + // Get all features with an enabled input processor? + if let Some(input_processor_system_features) = + vc_toy_features.get_features_with_input_processors(&msg.addr) + { + match newest_msg_val { + OscType::Float(lvl) => { + for feature in input_processor_system_features { + let float_level = ((lvl * 100.0).round() / 100.0) as f64; + // pen_system is checked for None in get_features_with_penetration_systems method. + // Give access to internal mode values here (input, internal_values) + if let Some(i_mode_processed_value) = feature + .penetration_system + .pen_system + .as_mut() + .unwrap() + .process( + msg.addr.as_str(), + ModeProcessorInputType::Float(float_level), + ) + { + // Send to mode processor if specified (Raw = no mode processing) + if let ProcessingMode::Raw = + feature.penetration_system.pen_system_processing_mode + { + command_toy( + dev.clone(), + feature.feature_type, + i_mode_processed_value, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } else { + // If mode processor returns a value send to toy + if let Some(i) = mode_processor( + ModeProcessorInput::InputProcessor(( + ModeProcessorInputType::Float(i_mode_processed_value), + &mut feature + .penetration_system + .pen_system_processing_mode_values, + )), + feature.feature_levels, + feature.flip_input_float, + ) + .await + { + command_toy( + dev.clone(), + feature.feature_type, + i, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } + } + } + } } - - // Create OSC listener thread - let toy_bcst_tx_osc = toy_bcst_tx.clone(); - info!("Spawning OSC listener.."); - let vc_conf_clone = vc_config.clone(); - let tme_send_clone = tme_send.clone(); - let app_handle_clone = app_handle.clone(); - thread::spawn(move || { - toy_input_routine( - toy_bcst_tx_osc, - tme_send_clone, - app_handle_clone, - vc_conf_clone, - ) - }); - - loop { - // Recv event (listening) - let event = tme_recv.recv().await; - match event { - Some(event) => { - match event { - // Handle Toy Update Signals - ToyManagementEvent::Tu(tu) => { - match tu { - ToyUpdate::AddToy(toy) => { - core_toy_manager - .online_toys - .insert(toy.toy_id, toy.clone()); - let f_run = f( - toy.device_handle, - toy_bcst_tx.subscribe(), - toy.parsed_toy_features.clone(), - ); - running_toy_ths.insert( - toy.toy_id, - toy_async_rt.spawn(async move { - f_run.await; - }), - ); - info!("Toy: {} started listening..", toy.toy_id); - } - ToyUpdate::RemoveToy(id) => { - // OSC Listener thread will only die on StopListening event - if let Some(toy) = running_toy_ths.remove(&id) { - toy.abort(); - match toy.await { - Ok(()) => info!("Toy {} thread finished", id), - Err(e) => warn!( - "Toy {} thread failed to reach completion: {}", - id, e - ), - } - info!("[TOY ID: {}] Stopped listening. (ToyUpdate::RemoveToy)", id); - running_toy_ths.remove(&id); - core_toy_manager.online_toys.remove(&id); - } - } - ToyUpdate::AlterToy(toy) => { - match toy_bcst_tx.send(ToySig::UpdateToy( - ToyUpdate::AlterToy(toy.clone()), - )) { - Ok(receivers) => info!( - "Sent ToyUpdate broadcast to {} toys", - receivers - 1 - ), - Err(e) => { - logerr!("Failed to send UpdateToy: {}", e) - } - } - core_toy_manager.online_toys.insert(toy.toy_id, toy); - } - } + // Boolean can be supported in the process trait method + OscType::Bool(b) => { + for feature in input_processor_system_features { + // Boolean to float transformation here + if let Some(i_mode_processed_value) = feature + .penetration_system + .pen_system + .as_mut() + .unwrap() + .process(msg.addr.as_str(), ModeProcessorInputType::Boolean(b)) + { + // Send to mode processor if specified (Raw = no mode processing) + if let ProcessingMode::Raw = + feature.penetration_system.pen_system_processing_mode + { + command_toy( + dev.clone(), + feature.feature_type, + i_mode_processed_value, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } else if let Some(i) = mode_processor( + ModeProcessorInput::InputProcessor(( + ModeProcessorInputType::Float(i_mode_processed_value), + &mut feature.penetration_system.pen_system_processing_mode_values, + )), + feature.feature_levels, + feature.flip_input_float, + ) + .await + { + command_toy( + dev.clone(), + feature.feature_type, + i, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } + } + } + } + _ => (), + } // End match OscType for Input processors + } // End Input processing + + if let Some(features) = vc_toy_features.get_features_from_param(&msg.addr) { + match newest_msg_val { + OscType::Float(lvl) => { + // Clamp float accuracy to hundredths and cast as 64 bit float + let float_level = ((lvl * 100.0).round() / 100.0) as f64; + //debug!("Received and cast float lvl: {:.5}", float_level); + + for feature in features { + // Get ToyParameter here + // We unwrap here because the call to get_features_from_param guarantees the parameter exists. + let mut toy_parameter = feature + .osc_parameters + .iter_mut() + .filter_map(|param| { + if param.parameter == msg.addr { + Some(param) + } else { + None } - // Handle Management Signals - ToyManagementEvent::Sig(tm_sig) => { - match tm_sig { - TmSig::StartListening(osc_net) => { - vc_config = osc_net; - // Already listening - } - TmSig::StopListening => { - // Stop listening on every device and clean running thread hashmap - - for toy in &mut running_toy_ths { - toy.1.abort(); - match toy.1.await { - Ok(()) => { - info!("Toy {} thread finished", toy.0) - } - Err(e) => warn!( - "Toy {} thread failed to reach completion: {}", - toy.0, e - ), - } - info!("[TOY ID: {}] Stopped listening. (TMSIG)", toy.0); - } - running_toy_ths.clear(); - drop(_toy_bcst_rx); // Causes OSC listener to die - toy_async_rt.shutdown_background(); - listening = false; - info!("Toys: {}", core_toy_manager.online_toys.len()); - break; //Stop Listening - } - TmSig::TMHReset => { - // Stop listening on every device and clean running thread hashmap - info!("TMHReset"); - - for toy in &mut running_toy_ths { - toy.1.abort(); - match toy.1.await { - Ok(()) => { - info!("Toy {} thread finished", toy.0) - } - Err(e) => warn!( - "Toy {} thread failed to reach completion: {}", - toy.0, e - ), - } - info!("[TOY ID: {}] Stopped listening. (TMSIG)", toy.0); - } - running_toy_ths.clear(); - drop(_toy_bcst_rx); // Causes OSC listener to die - toy_async_rt.shutdown_background(); - listening = false; - info!("Toys: {}", core_toy_manager.online_toys.len()); - break; //Stop Listening - } - _ => {} - } + }) + .collect::>(); + + if let Some(first_toy_param) = toy_parameter.first_mut() { + if let Some(mode_processed_value) = mode_processor( + ModeProcessorInput::RawInput( + ModeProcessorInputType::Float(float_level), + first_toy_param, + ), + feature.feature_levels, + feature.flip_input_float, + ) + .await + { + command_toy( + dev.clone(), + feature.feature_type, + mode_processed_value, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } + } // If no matching toy parameter skip feature + } + } + OscType::Bool(b) => { + info!("Got a Bool! {} = {}", msg.addr, b); + for feature in features { + // Get ToyParameter here + let mut toy_parameter = feature + .osc_parameters + .iter_mut() + .filter_map(|param| { + if param.parameter == msg.addr { + Some(param) + } else { + None } - } // Event handled + }) + .collect::>(); + + if let Some(first_toy_param) = toy_parameter.first_mut() { + if let Some(i) = mode_processor( + ModeProcessorInput::RawInput( + ModeProcessorInputType::Boolean(b), + first_toy_param, + ), + feature.feature_levels, + feature.flip_input_float, + ) + .await + { + command_toy( + dev.clone(), + feature.feature_type, + i, + feature.feature_index, + feature.flip_input_float, + feature.feature_levels, + ) + .await; + } } - None => {} } } - } //if listening - } // Management loop + _ => {} // Skip parameter because unsuppported OSC type + } + } +} + +#[inline(always)] +fn update_toy(toy: ToyUpdate, dev: Arc, vc_toy_features: &mut VCToyFeatures) { + let ToyUpdate::AlterToy(new_toy) = toy else { + return; + }; + if new_toy.toy_id != dev.index() { + return; + } + *vc_toy_features = new_toy.parsed_toy_features; + info!("Altered toy: {}", new_toy.toy_id); } /* diff --git a/src-tauri/src/toy_handling/input_processor/penetration_systems/mod.rs b/src-tauri/src/toy_handling/input_processor/penetration_systems/mod.rs index f3a2778..be96870 100644 --- a/src-tauri/src/toy_handling/input_processor/penetration_systems/mod.rs +++ b/src-tauri/src/toy_handling/input_processor/penetration_systems/mod.rs @@ -59,8 +59,8 @@ impl FromFrontend for PenetrationSystem { // Allocate / Instantiate new Penetration system structure based on user's choice match frontend_type.pen_system_type { PenetrationSystemType::NONE => self.pen_system = None, - PenetrationSystemType::SPS => self.pen_system = Some(Box::new(SPSProcessor::default())), - PenetrationSystemType::TPS => self.pen_system = Some(Box::new(TPSProcessor::default())), + PenetrationSystemType::SPS => self.pen_system = Some(Box::::default()), + PenetrationSystemType::TPS => self.pen_system = Some(Box::::default()), } self.pen_system_type = frontend_type.pen_system_type; let backend_pspm = frontend_type.pen_system_processing_mode.to_backend(); diff --git a/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mapping.rs b/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mapping.rs new file mode 100644 index 0000000..bdc28fb --- /dev/null +++ b/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mapping.rs @@ -0,0 +1,429 @@ +use log::{debug, trace, warn}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::{cmp::Ordering, collections::HashMap}; +use strum::{Display, EnumString}; +use ts_rs::TS; + +use crate::toy_handling::ModeProcessorInputType; + +use super::SPSWho; + +const SAVED_LENGTH_VALUES_MAX: usize = 8; +const SAVED_LENGTH_VALUES_MIN: usize = 4; + +// Orifice's (TYPE,LEAF) +const ORF_TOUCHOTHERSCLOSE: (SPSParameterType, &str) = (SPSParameterType::Orf, "TouchOthersClose"); +const ORF_TOUCHSELFCLOSE: (SPSParameterType, &str) = (SPSParameterType::Orf, "TouchSelfClose"); +const ORF_TOUCHSELF: (SPSParameterType, &str) = (SPSParameterType::Orf, "TouchSelf"); +const ORF_TOUCHOTHERS: (SPSParameterType, &str) = (SPSParameterType::Orf, "TouchOthers"); +const ORF_PENSELF: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenSelf"); +const ORF_PENOTHERSCLOSE: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenOthersClose"); +const ORF_PENOTHERS: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenOthers"); +const ORF_FROTOTHERS: (SPSParameterType, &str) = (SPSParameterType::Orf, "FrotOthers"); +const ORF_PENSELFNEWROOT: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenSelfNewRoot"); +const ORF_PENOTHERSNEWROOT: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenOthersNewRoot"); +const ORF_PENSELFNEWTIP: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenSelfNewTip"); +const ORF_PENOTHERSNEWTIP: (SPSParameterType, &str) = (SPSParameterType::Orf, "PenOthersNewTip"); + +// Penetrator's (TYPE,LEAF) +const PEN_TOUCHSELFCLOSE: (SPSParameterType, &str) = (SPSParameterType::Pen, "TouchSelfClose"); +const PEN_TOUCHOTHERSCLOSE: (SPSParameterType, &str) = (SPSParameterType::Pen, "TouchOthersClose"); +const PEN_TOUCHSELF: (SPSParameterType, &str) = (SPSParameterType::Pen, "TouchSelf"); +const PEN_TOUCHOTHERS: (SPSParameterType, &str) = (SPSParameterType::Pen, "TouchOthers"); +const PEN_PENSELF: (SPSParameterType, &str) = (SPSParameterType::Pen, "PenSelf"); +const PEN_PENOTHERS: (SPSParameterType, &str) = (SPSParameterType::Pen, "PenOthers"); +const PEN_FROTOTHERSCLOSE: (SPSParameterType, &str) = (SPSParameterType::Pen, "FrotOthersClose"); +const PEN_FROTOTHERS: (SPSParameterType, &str) = (SPSParameterType::Pen, "FrotOthers"); + +// Touch's (TYPE,LEAF) +const TOUCH_SELF: (SPSParameterType, &str) = (SPSParameterType::Touch, "Self"); +const TOUCH_OTHERS: (SPSParameterType, &str) = (SPSParameterType::Touch, "Others"); + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS, Display, EnumString, PartialEq, Eq)] +pub enum SPSParameterType { + Orf, + Pen, + Touch, +} + +impl SPSParameterType { + pub fn is_orf(&self) -> bool { + match self { + Self::Orf => true, + _ => false, + } + } + pub fn is_pen(&self) -> bool { + match self { + Self::Pen => true, + _ => false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, TS)] +pub struct SPSMapping { + // Orf || Pen || Touch + pub param_type: SPSParameterType, + // The mesh name / identifier for the orifice or penetrator (blowjob/anal/etc.) + param_obj_id: String, + + length_values_others: Vec, + others_stored_length: f64, + length_values_self: Vec, + self_stored_length: f64, + + pub others_touch_enabled: bool, + pub self_touch_enabled: bool, + pub legacy_orf_enabled: bool, + pub pen_frot_others: bool, + // K: Contact type | V: OSCValue + // Values are leaf param's values + osc_values: HashMap, +} + +impl SPSMapping { + pub fn new(sps_type: String, sps_obj_id: String) -> Option { + let param_type = + SPSParameterType::from_str(&sps_type).expect("parameter_type convert enum string"); + let param_obj_id = sps_obj_id; + + Some(Self { + param_type, + param_obj_id, + length_values_others: Vec::with_capacity(SAVED_LENGTH_VALUES_MAX + 1), + others_stored_length: 0., + length_values_self: Vec::with_capacity(SAVED_LENGTH_VALUES_MAX + 1), + self_stored_length: 0., + others_touch_enabled: false, + self_touch_enabled: false, + legacy_orf_enabled: false, + pen_frot_others: false, + osc_values: HashMap::new(), + }) + } + + pub fn parse_features_get_who(&mut self, leaf: &str, input: ModeProcessorInputType) -> SPSWho { + let support_map = (self.param_type, leaf); + + let others: SPSWho = match support_map { + /* + * -= Unsupported =- + * + * Self + * Others + */ + // -= "New SPS tip/root" =- + ORF_PENOTHERSNEWROOT | ORF_PENOTHERSNEWTIP => SPSWho::Others, + ORF_PENSELFNEWROOT | ORF_PENSELFNEWTIP => SPSWho::_Self, + // -= SPS Frot =- + PEN_FROTOTHERSCLOSE => { + let Some(b) = input.try_bool() else { + return SPSWho::Pass; + }; + + if b { + self.pen_frot_others = true; + } else { + self.pen_frot_others = false; + return SPSWho::Stop; + } + + SPSWho::Others + } + ORF_FROTOTHERS => SPSWho::Bypass(input.try_float()), + PEN_FROTOTHERS => { + if self.pen_frot_others { + // Is frotting others + SPSWho::Others + } else { + // Is not frotting others + SPSWho::Pass + } + } + // -= SPS Legacy =- + ORF_PENOTHERSCLOSE => { + // Only applies to orifices.. Pen type only uses PenOthers/PenSelf and not length calcs + + let Some(b) = input.try_bool() else { + return SPSWho::Pass; + }; + + if b { + self.legacy_orf_enabled = true; + } else { + self.legacy_orf_enabled = false; + return SPSWho::Stop; + } + SPSWho::Others + } + ORF_PENSELF | PEN_PENSELF => SPSWho::_Self, + PEN_PENOTHERS | ORF_PENOTHERS => SPSWho::Others, + // -= SPS Touch =- + ORF_TOUCHOTHERSCLOSE | PEN_TOUCHOTHERSCLOSE => { + // If this parameter is not a bool then skip and return None + let Some(b) = input.try_bool() else { + return SPSWho::Pass; + }; + + if b { + self.others_touch_enabled = true; + } else { + self.others_touch_enabled = false; + return SPSWho::Stop; + } + + SPSWho::Others + } + ORF_TOUCHSELFCLOSE | PEN_TOUCHSELFCLOSE => { + // If this parameter is not a bool then skip and return None + let Some(b) = input.try_bool() else { + // Parameter was wrong type so pass calculation + return SPSWho::Pass; + }; + + if b { + self.self_touch_enabled = true; + } else { + self.self_touch_enabled = false; + return SPSWho::Stop; + } + + SPSWho::_Self + } + ORF_TOUCHOTHERS | PEN_TOUCHOTHERS => SPSWho::Others, + ORF_TOUCHSELF | PEN_TOUCHSELF => SPSWho::_Self, + TOUCH_SELF => SPSWho::_Self, + TOUCH_OTHERS => SPSWho::Others, + _ => { + warn!( + "No Leaf Match for Other | Self - Unhandled OGB parameter?: {}", + leaf + ); + SPSWho::Pass + } + }; + + debug!( + "WHO: {:?} | TOUCH: O{}/S{}", + others, self.others_touch_enabled, self.self_touch_enabled + ); + others + } + + fn get_root_tip_osc_values(&self, others: SPSWho) -> Option<(f64, f64)> { + let values: (f64, f64) = if let SPSWho::Others = others { + let root_value = self.osc_values.get("PenOthersNewRoot")?.try_float()?; + let tip_value = self.osc_values.get("PenOthersNewTip")?.try_float()?; + (root_value, tip_value) + } else { + let root_value = self.osc_values.get("PenSelfNewRoot")?.try_float()?; + let tip_value = self.osc_values.get("PenSelfNewTip")?.try_float()?; + (root_value, tip_value) + }; + + Some(values) + } + + pub fn is_touch(&self) -> bool { + self.others_touch_enabled || self.self_touch_enabled + } + + pub fn is_frot(&self) -> bool { + self.pen_frot_others + } + + pub fn is_legacy_orf(&self) -> bool { + if self.legacy_orf_enabled { + if let SPSParameterType::Orf = self.param_type { + return true; + } + } + false + } + + pub fn get_frot_value(&self) -> Option { + /* + if !self.is_frot() { + return None; + }*/ + + // There is no FrotSelf + self.osc_values.get("FrotOthers")?.try_float() + } + + pub fn get_touch_value(&self, others: SPSWho) -> Option { + /* + if !self.is_touch() { + return None; + }*/ + + if let SPSWho::Others = others { + self.osc_values.get("TouchOthers")?.try_float() + } else { + self.osc_values.get("TouchSelf")?.try_float() + } + } + + pub fn get_legacy_value(&self, others: SPSWho) -> Option { + if let SPSWho::Others = others { + self.osc_values.get("PenOthers")?.try_float() + } else { + self.osc_values.get("PenSelf")?.try_float() + } + } + + pub fn add_osc_value(&mut self, leaf: String, value: ModeProcessorInputType) { + self.osc_values.insert(leaf, value); + } + + pub fn update_mapping_length_values(&mut self, others: SPSWho) { + // Logic needs to be optimized (polymorphism/whatever) + let Some((root_value, tip_value)) = self.get_root_tip_osc_values(others) else { + debug!("Failed to get root & tip values!"); + if let SPSWho::Others = others { + self.length_values_others.clear(); + } else { + self.length_values_self.clear(); + } + return; + }; + trace!("Got root & tip values"); + + if root_value < 0.01 || tip_value < 0.01 { + if let SPSWho::Others = others { + self.length_values_others.clear(); + } else { + self.length_values_self.clear(); + } + } + + if root_value > 0.95 { + return; + } + + let temp_length = tip_value - root_value; + debug!("Length Calculation: {}", temp_length); + + if temp_length < 0.02 { + return; + } + + if tip_value > 0.99 { + // Use last value + // Should we have a bad length ? + // No need for bad length just reuse last ?? + } else { + if let SPSWho::Others = others { + self.length_values_others.insert(0, temp_length); + self.length_values_others.truncate(SAVED_LENGTH_VALUES_MAX); + debug!("Added length value"); + } else { + self.length_values_self.insert(0, temp_length); + self.length_values_self.truncate(SAVED_LENGTH_VALUES_MAX); + } + } + } + + pub fn update_mapping_length(&mut self, others: SPSWho) { + let values_len = if let SPSWho::Others = others { + self.length_values_others.len() + } else { + self.length_values_self.len() + }; + // Enough stored length values? + if values_len < SAVED_LENGTH_VALUES_MIN { + // Just don't update length? Or give a bad length? + return; + } + + if let SPSWho::Others = others { + self.length_values_others + .sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); + + let stacks: Vec<&[f64]> = self.length_values_others.windows(2).collect(); + + if stacks.len() < 2 { + return; + } + + let mut smallest_diff = 1.; + let mut iterator = 0; + + for stack in &stacks { + let diff = (stack[1] - stack[0]).abs(); + if diff < smallest_diff { + smallest_diff = diff; + iterator += 1; + } + } + + self.others_stored_length = stacks[iterator - 1][0]; + debug!("Length Calculated! {}", self.others_stored_length); + } else { + self.length_values_self + .sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); + + let stacks: Vec<&[f64]> = self.length_values_self.windows(2).collect(); + + let mut smallest_diff = 1.; + let mut iterator = 0; + + for stack in &stacks { + let diff = (stack[1] - stack[0]).abs(); + if diff < smallest_diff { + smallest_diff = diff; + iterator += 1; + } + } + + self.self_stored_length = stacks[iterator][0]; + debug!("Length Calculated! {}", self.self_stored_length); + } + } + + pub fn update_level(&mut self, others: SPSWho) -> Option { + // If mapping is in touch mode + if self.is_touch() { + let new_touch_value = self.get_touch_value(others)?; + debug!("TOUCH VALUE: {}", new_touch_value); + return Some(new_touch_value); + } + + // If mapping is in frot mode (NO OTHER) + if self.is_frot() { + let new_frot_value = self.get_frot_value()?; + debug!("FROT VALUE: {}", new_frot_value); + return Some(new_frot_value); + } + + // Mapping is legacy orifice or mapping is a Pen type + // I will assume here that legacy orf mode applies to PenOthers & PenSelf + // Assuming that when someone is PenSelf they are using the Self Tip/Root method + if self.is_legacy_orf() || self.param_type.is_pen() { + let new_legacy_value = self.get_legacy_value(others)?; + debug!("LEGACY VALUE: {}", new_legacy_value); + return Some(new_legacy_value); + } + + let (root_value, tip_value) = self.get_root_tip_osc_values(others)?; + let stored_length = if let SPSWho::Others = others { + self.others_stored_length + } else { + self.self_stored_length + }; + debug!("Stored Length: {}", stored_length); + + if stored_length > 0. && tip_value > 0.99 { + let active_length = 1. - root_value; + let active_ratio = active_length / stored_length; + let level = 1. - active_ratio; + debug!("SPS Calculated Level: {}", level); + return Some(level); + } + + debug!("Did not update level!"); + None + } +} diff --git a/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mod.rs b/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mod.rs index 19ec556..721cc30 100644 --- a/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mod.rs +++ b/src-tauri/src/toy_handling/input_processor/penetration_systems/sps/mod.rs @@ -1,27 +1,149 @@ +pub mod mapping; + +use std::collections::HashMap; + +use log::{trace, warn}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::toy_handling::{input_processor::InputProcessor, ModeProcessorInputType}; -#[derive(Clone, Debug, Serialize, Deserialize, TS)] +use self::mapping::SPSMapping; + +#[derive(Debug, Copy, Clone)] +pub enum SPSWho { + Others, + _Self, + Pass, + Bypass(Option), + Stop, // For when a boolean param is zeroed we need to zero the power due to in game inaccuracies +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, TS)] pub struct SPSProcessor { - pub parameter_list: Vec, + mappings: HashMap, } -impl Default for SPSProcessor { - fn default() -> Self { - Self { - parameter_list: vec![], +impl InputProcessor for SPSProcessor { + fn is_parameter(&self, param: &String) -> bool { + param.starts_with("/avatar/parameters/OGB/") + } + + /** + * Inner workings of SPS according to SPS creator's app OGB (https://github.com/OscToys/OscGoesBrrr) + * + * There seems to be two parameter leafs that are considered 'Legacy' (*OthersClose) & (*Others) + few more + * + * addKey(OSC K,V) -> Overwrites values via param leaf key + * onKeyChange() -> Tests for Self|Other && NewRoot|NewTip + * Length Detectors -> update() method(NewRoot.value, NewTip.value) + * within update() -> test for bad samples (set bad sample) -> get length (len = NewTip.value - NewRoot.value) -> save if tip <= 0.99 + * calculate length from samples -> if less than 4 samples use old / bad sample if <4 + * if 4 or more samples do length decision algo + * motor level is calculated by + * ~ + * activeDistance = 1 - NewRoot.value + * activeRatio = activeDistance / saved_mapping_length + * level = 1 - activeRatio + * ~ + * + */ + + fn process(&mut self, addr: &str, input: ModeProcessorInputType) -> Option { + // Don't support booleans + //let ModeProcessorInputType::Float(float_input) = input else { + // return None; + //}; + + // Strip away VRChat avatar parameter prefix + let sps_param = addr.strip_prefix("/avatar/parameters/")?; + trace!("SPS Param: {}", sps_param); + + // Process SPS Param object key + //let (sps_key, sps_leaf, sps_type) = SPSProcessor::get_sps_param_key_leaf(sps_param)?; + //debug!("SPS Key: {} | SPS Leaf: {}", sps_key, sps_leaf); + + // Process parameter and create or get mutable ref to mapping + let (mapping, _sps_type, leaf) = self.populate_mapping(&sps_param, input)?; + + let others = mapping.parse_features_get_who(leaf.as_str(), input); + + match others { + SPSWho::Pass => return None, // OSC Address does not apply to below calculations + SPSWho::Stop => return Some(0.), // Stop feature's motor due to a bool flag being flipped + SPSWho::Bypass(l) => return l, + _ => {} } + + if !mapping.is_touch() && !mapping.is_legacy_orf() && !mapping.is_frot() { + // Add good length calculations to mapping (self/other) + mapping.update_mapping_length_values(others); + // Update internal mapping length based on stored length calculations + mapping.update_mapping_length(others); + } + // Get updated feature level (bzz level) + mapping.update_level(others) } } -impl InputProcessor for SPSProcessor { - fn is_parameter(&self, param: &String) -> bool { - self.parameter_list.contains(param) +impl SPSProcessor { + fn get_sps_param_parsed(osc_addr: &str) -> Option<(String, String, String, String)> { + let sps_param_split = osc_addr.split('/').collect::>(); + + if sps_param_split.len() != 4 { + return None; + }; + + // Orf/Pen/etc. + let p_type = sps_param_split[1]; + + // Unity Object name + let p_id = sps_param_split[2]; + + // Contact Leaf + let leaf = sps_param_split[3]; + + Some(( + // SPS Key + format!("{}__{}", p_type, p_id), + // SPS Type + p_type.to_string(), + // SPS Obj ID + p_id.to_string(), + // Contact Leaf + leaf.to_string(), + )) } - fn process(&mut self, _addr: &str, _input: ModeProcessorInputType) -> Option { - todo!() + fn populate_mapping( + &mut self, + sps_param: &str, + osc_input_value: ModeProcessorInputType, + ) -> Option<(&mut SPSMapping, String, String)> { + let Some((sps_key, sps_type, _sps_obj_id, sps_leaf)) = + SPSProcessor::get_sps_param_parsed(&sps_param) + else { + return None; + }; + + if !self.mappings.contains_key(&sps_key) { + let Some(new_sps_param_obj) = + SPSMapping::new(sps_type.to_string(), sps_type.to_string()) + else { + warn!("Failed to create mapping!"); + return None; + }; + + self.mappings.insert(sps_key.to_string(), new_sps_param_obj); + } + + let mut mapping = self.mappings.get_mut(&sps_key); + + mapping + .as_mut() + .unwrap() + .add_osc_value(sps_leaf.to_string(), osc_input_value); + + Some((mapping.unwrap(), sps_type, sps_leaf)) } } diff --git a/src-tauri/src/toy_handling/input_processor/penetration_systems/tps/mod.rs b/src-tauri/src/toy_handling/input_processor/penetration_systems/tps/mod.rs index d1784eb..5ce9823 100644 --- a/src-tauri/src/toy_handling/input_processor/penetration_systems/tps/mod.rs +++ b/src-tauri/src/toy_handling/input_processor/penetration_systems/tps/mod.rs @@ -3,25 +3,23 @@ use ts_rs::TS; use crate::toy_handling::{input_processor::InputProcessor, ModeProcessorInputType}; -#[derive(Clone, Debug, Serialize, Deserialize, TS)] +#[derive(Default, Clone, Debug, Serialize, Deserialize, TS)] pub struct TPSProcessor { pub parameter_list: Vec, } -impl Default for TPSProcessor { - fn default() -> Self { - Self { - parameter_list: vec![], - } - } -} - impl InputProcessor for TPSProcessor { fn is_parameter(&self, param: &String) -> bool { - self.parameter_list.contains(param) + param.starts_with("/avatar/parameters/TPS_Internal/") } - fn process(&mut self, _addr: &str, _input: ModeProcessorInputType) -> Option { - todo!() + fn process(&mut self, addr: &str, input: ModeProcessorInputType) -> Option { + let tps_param = addr.strip_prefix("/avatar/parameters/")?; + + if tps_param.ends_with("Depth_In") || tps_param.ends_with("RootRoot") { + input.try_float() + } else { + None + } } } diff --git a/src-tauri/src/toy_handling/mod.rs b/src-tauri/src/toy_handling/mod.rs index f621210..788981d 100644 --- a/src-tauri/src/toy_handling/mod.rs +++ b/src-tauri/src/toy_handling/mod.rs @@ -32,28 +32,26 @@ pub enum ModeProcessorInput<'processor> { RawInput(ModeProcessorInputType, &'processor mut ToyParameter), } -/* -impl<'processor> ModeProcessorInput<'processor> { - fn is_input_processor(&self) -> bool { - if let ModeProcessorInput::InputProcessor(_) = self { - true - } else { - false +#[derive(Debug, Clone, TS, Serialize, Deserialize, Copy)] +pub enum ModeProcessorInputType { + Float(f64), + Boolean(bool), +} + +impl ModeProcessorInputType { + pub fn try_float(&self) -> Option { + match self { + Self::Float(f) => Some(*f), + _ => None, } } - fn is_raw_input(&self) -> bool { - if let ModeProcessorInput::RawInput(_, _) = self { - true - } else { - false + pub fn try_bool(&self) -> Option { + match self { + Self::Boolean(b) => Some(*b), + _ => None, } } -}*/ - -pub enum ModeProcessorInputType { - Float(f64), - Boolean(bool), } #[derive(Debug, Serialize, Deserialize, TS, Clone)] @@ -80,7 +78,7 @@ impl ToString for ToyPower { Self::Pending => "Pending".to_owned(), Self::Battery(level) => { let m = 100.0 * level; - format!("{}%", m.to_string()) + format!("{}%", m) } Self::NoBattery => "Powered".to_owned(), Self::Offline => "Offline".to_owned(), diff --git a/src-tauri/src/toy_handling/toyops.rs b/src-tauri/src/toy_handling/toyops.rs index 97d3170..444c13c 100644 --- a/src-tauri/src/toy_handling/toyops.rs +++ b/src-tauri/src/toy_handling/toyops.rs @@ -107,8 +107,8 @@ impl VCToy { .iter() .for_each(|scalar_feature| { // Filter out Rotators - match scalar_feature.actuator_type() { - &ActuatorType::Rotate => { + match *scalar_feature.actuator_type() { + ActuatorType::Rotate => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -117,7 +117,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Vibrate => { + ActuatorType::Vibrate => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -126,7 +126,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Constrict => { + ActuatorType::Constrict => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -135,7 +135,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Inflate => { + ActuatorType::Inflate => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -144,7 +144,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Oscillate => { + ActuatorType::Oscillate => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -153,7 +153,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Position => { + ActuatorType::Position => { self.parsed_toy_features.features.push(VCToyFeature::new( vec![], indexer, @@ -162,7 +162,7 @@ impl VCToy { indexer += 1; } - &ActuatorType::Unknown => {} + ActuatorType::Unknown => {} } }); info!("Populated {} scalars", indexer); @@ -242,11 +242,11 @@ impl VCToy { PenetrationSystemType::NONE => feature.penetration_system.pen_system = None, PenetrationSystemType::SPS => { feature.penetration_system.pen_system = - Some(Box::new(SPSProcessor::default())) + Some(Box::::default()) } PenetrationSystemType::TPS => { feature.penetration_system.pen_system = - Some(Box::new(TPSProcessor::default())) + Some(Box::::default()) } } @@ -278,7 +278,7 @@ impl VCToy { if !file_exists(&config_path) { self.config = None; - return Ok(()); + Ok(()) } else { let con = fs::read_to_string(config_path).unwrap(); @@ -291,7 +291,7 @@ impl VCToy { }; debug!("Loaded & parsed toy config successfully!"); self.config = Some(config); - return Ok(()); + Ok(()) } } @@ -309,11 +309,9 @@ impl VCToy { match fs::write(&config_path, json_string) { Ok(()) => { info!("Saved toy config: {}", self.toy_name); - return; } Err(e) => { logerr!("Failed to write to file: {}", e); - return; } } } else { @@ -334,7 +332,7 @@ impl VCToy { }); return true; } - return false; + false } } @@ -408,11 +406,7 @@ pub struct ToyParameter { impl ToyParameter { fn is_assigned_param(&self, param: &String) -> bool { - if self.parameter == *param { - true - } else { - false - } + self.parameter == *param } } @@ -499,7 +493,7 @@ impl VCToyFeature { } } } - return None; + None } /* @@ -597,10 +591,6 @@ impl PartialEq for VCFeatureType { fn eq(&self, other: &FeVCFeatureType) -> bool { *self as u32 == *other as u32 } - - fn ne(&self, other: &FeVCFeatureType) -> bool { - !self.eq(other) - } } impl VCFeatureType { @@ -753,7 +743,7 @@ impl VCToyFeatures { for f in &mut self.features { // If penetration system not set for feature move to next feature - if let None = f.penetration_system.pen_system { + if f.penetration_system.pen_system.is_none() { continue; } diff --git a/src-tauri/src/util/bluetooth.rs b/src-tauri/src/util/bluetooth.rs index c3a4514..bfc44aa 100644 --- a/src-tauri/src/util/bluetooth.rs +++ b/src-tauri/src/util/bluetooth.rs @@ -5,28 +5,26 @@ use buttplug::core::connector::ButtplugInProcessClientConnectorBuilder; //new_js use buttplug::server::device::hardware::communication::btleplug::BtlePlugCommunicationManagerBuilder; use buttplug::server::device::hardware::communication::lovense_connect_service::LovenseConnectServiceCommunicationManagerBuilder; use buttplug::server::ButtplugServerBuilder; -//use buttplug::server::device::hardware::communication::websocket_server::websocket_server_comm_manager::WebsocketServerDeviceCommunicationManagerBuilder; use log::{error as logerr, info, trace, warn}; #[allow(unused)] pub async fn detect_btle_adapter() -> bool { - if let Ok(manager) = Manager::new().await { - if let Ok(adapters) = manager.adapters().await { - if adapters.is_empty() { - return false; - } - let adapter = manager.adapters().await.unwrap(); - let central = adapter.into_iter().nth(0).unwrap(); - info!("[+] BTLE: {}", central.adapter_info().await.unwrap()); - return !adapters.is_empty(); - } else { - warn!("No btle adapters detected"); - return false; - } - } else { + let Ok(manager) = Manager::new().await else { logerr!("[-] Failed to create btle Manager."); return false; + }; + let Ok(adapters) = manager.adapters().await else { + warn!("No btle adapters detected"); + return false; + }; + if adapters.is_empty() { + return false; } + + let adapter = manager.adapters().await.unwrap(); + let central = adapter.into_iter().next().unwrap(); + info!("[+] BTLE: {}", central.adapter_info().await.unwrap()); + !adapters.is_empty() // TODO is this always true? } pub async fn vc_toy_client_server_init( diff --git a/src-tauri/src/util/net.rs b/src-tauri/src/util/net.rs index 3103993..4ad7962 100644 --- a/src-tauri/src/util/net.rs +++ b/src-tauri/src/util/net.rs @@ -7,14 +7,8 @@ pub enum InterfaceL4Proto { fn is_port_available(interface_proto: InterfaceL4Proto, port: u16) -> bool { match interface_proto { - InterfaceL4Proto::TCP(interface) => match TcpListener::bind((interface, port)) { - Ok(_) => true, - _ => false, - }, - InterfaceL4Proto::UDP(interface) => match UdpSocket::bind((interface, port)) { - Ok(_) => true, - _ => false, - }, + InterfaceL4Proto::TCP(interface) => TcpListener::bind((interface, port)).is_ok(), + InterfaceL4Proto::UDP(interface) => UdpSocket::bind((interface, port)).is_ok(), } } diff --git a/src-tauri/src/vcore/config.rs b/src-tauri/src/vcore/config.rs index 7233959..e1b88eb 100644 --- a/src-tauri/src/vcore/config.rs +++ b/src-tauri/src/vcore/config.rs @@ -45,6 +45,8 @@ pub struct VibeCheckConfig { pub minimize_on_exit: bool, pub desktop_notifications: bool, pub lc_override: Option, + pub show_toy_advanced: bool, + pub show_feature_advanced: bool, } pub fn config_load() -> VibeCheckConfig { @@ -76,6 +78,8 @@ pub fn config_load() -> VibeCheckConfig { minimize_on_exit: false, desktop_notifications: false, lc_override: None, + show_toy_advanced: false, + show_feature_advanced: false, }) .unwrap(), ) @@ -90,16 +94,10 @@ pub fn config_load() -> VibeCheckConfig { Ok(o) => { info!("Config Loaded Successfully!"); if let Some(h) = o.lc_override { - std::env::set_var( - "VCLC_HOST_PORT", - format!("{}:20010", h.to_string()).as_str(), - ); - info!( - "Setting VCLC_HOST_PORT: {}", - format!("{}:20010", h.to_string()) - ); + std::env::set_var("VCLC_HOST_PORT", format!("{}:20010", h).as_str()); + info!("Setting VCLC_HOST_PORT: {}", format!("{}:20010", h)); } - return o; + o } Err(_e) => { logerr!( @@ -115,12 +113,14 @@ pub fn config_load() -> VibeCheckConfig { minimize_on_exit: false, desktop_notifications: false, lc_override: None, + show_toy_advanced: false, + show_feature_advanced: false, }; fs::write(&vc_config_file, serde_json::to_string(&def_conf).unwrap()).unwrap(); trace!("Wrote VibeCheck config file"); // If fail to parse config overwrite with new default - return def_conf; + def_conf } }, Err(_e) => { @@ -136,10 +136,12 @@ pub fn config_load() -> VibeCheckConfig { minimize_on_exit: false, desktop_notifications: false, lc_override: None, + show_toy_advanced: false, + show_feature_advanced: false }; fs::write(&vc_config_file, serde_json::to_string(&def_conf).unwrap()).unwrap(); trace!("Wrote VibeCheck config file"); - return def_conf; + def_conf } } } @@ -300,7 +302,7 @@ pub mod toy { ); if !file_exists(&config_path) { - return Err(vcerror::backend::VibeCheckToyConfigError::OfflineToyConfigNotFound); + Err(vcerror::backend::VibeCheckToyConfigError::OfflineToyConfigNotFound) } else { let con = std::fs::read_to_string(config_path).unwrap(); @@ -311,7 +313,7 @@ pub mod toy { } }; debug!("Loaded & parsed toy config successfully!"); - return Ok(config); + Ok(config) } } @@ -328,11 +330,9 @@ pub mod toy { match std::fs::write(&config_path, json_string) { Ok(()) => { info!("Saved toy config: {}", self.toy_name); - return; } Err(e) => { logerr!("Failed to write to file: {}", e); - return; } } } else { diff --git a/src-tauri/src/vcore/core.rs b/src-tauri/src/vcore/core.rs index 652e917..1e2772a 100644 --- a/src-tauri/src/vcore/core.rs +++ b/src-tauri/src/vcore/core.rs @@ -296,11 +296,11 @@ impl VibeCheckState { find_available_udp_port(self.config.networking.bind.ip().to_string()); let http_net = SocketAddrV4::new( - Ipv4Addr::from(*self.config.networking.bind.ip()), + *self.config.networking.bind.ip(), available_tcp_port.unwrap(), ); let osc_net = SocketAddrV4::new( - Ipv4Addr::from(*self.config.networking.bind.ip()), + *self.config.networking.bind.ip(), available_udp_port.unwrap(), ); @@ -379,7 +379,6 @@ pub async fn native_vibecheck_disable( return Err(frontend::VCFeError::DisableFailure); } - //Delay::new(Duration::from_secs(10)).await; trace!("Calling destroy_toy_update_handler()"); vc_lock.destroy_toy_update_handler().await; trace!("TUH destroyed"); @@ -387,27 +386,15 @@ pub async fn native_vibecheck_disable( let bpc = vc_lock.bp_client.as_ref().unwrap(); let _ = bpc.stop_scanning().await; let _ = bpc.stop_all_devices().await; - //let _ = bpc.disconnect().await; - //Delay::new(Duration::from_secs(10)).await; - //drop(bpc); - info!("ButtplugClient stopped operations"); - // CEH no longer gets destroyed - //trace!("Calling destroy_ceh()"); - //vc_lock.destroy_ceh().await; - //info!("CEH destroyed"); + info!("ButtplugClient stopped operations"); - //Delay::new(Duration::from_secs(10)).await; vc_lock .tme_send_tx .send(ToyManagementEvent::Sig(TmSig::TMHReset)) .unwrap(); info!("Sent TMHReset signal"); - // Dont clear toys anymore - //vc_lock.toys.clear(); - //info!("Cleared toys in VibeCheckState"); - //let _ = vc_lock.bp_client.as_ref().unwrap().stop_all_devices().await; vc_lock.running = RunningState::Stopped; info!("Starting disabled state OSC cmd listener"); @@ -470,8 +457,7 @@ pub async fn native_vibecheck_enable( .send(ToyManagementEvent::Sig(TmSig::StopListening)) .unwrap(); vc_lock.running = RunningState::Stopped; - - return Err(frontend::VCFeError::EnableBindFailure); + Err(frontend::VCFeError::EnableBindFailure) } _ => { //Did not get the correct signal oops @@ -613,13 +599,7 @@ pub fn native_get_vibecheck_config(vc_state: tauri::State<'_, VCStateMutex>) -> vc_lock.config.clone() }; - let lc_or = { - if let Some(host) = config.lc_override { - Some(host.to_string()) - } else { - None - } - }; + let lc_or = config.lc_override.map(|host| host.to_string()); FeVibeCheckConfig { networking: config.networking.to_fe(), @@ -627,6 +607,8 @@ pub fn native_get_vibecheck_config(vc_state: tauri::State<'_, VCStateMutex>) -> minimize_on_exit: config.minimize_on_exit, desktop_notifications: config.desktop_notifications, lc_override: lc_or, + show_toy_advanced: config.show_toy_advanced, + show_feature_advanced: config.show_feature_advanced, } } @@ -652,16 +634,15 @@ pub fn native_set_vibecheck_config( vc_lock.config.scan_on_disconnect = fe_vc_config.scan_on_disconnect; vc_lock.config.minimize_on_exit = fe_vc_config.minimize_on_exit; vc_lock.config.desktop_notifications = fe_vc_config.desktop_notifications; + vc_lock.config.show_toy_advanced = fe_vc_config.show_toy_advanced; + vc_lock.config.show_feature_advanced = fe_vc_config.show_feature_advanced; if let Some(host) = fe_vc_config.lc_override { // Is valid IPv4? match Ipv4Addr::from_str(&host) { Ok(sa) => { // Force port because buttplug forces non http atm - std::env::set_var( - "VCLC_HOST_PORT", - format!("{}:20010", sa.to_string()).as_str(), - ); + std::env::set_var("VCLC_HOST_PORT", format!("{}:20010", sa).as_str()); match std::env::var("VCLC_HOST_PORT") { Ok(_) => { vc_lock.config.lc_override = Some(sa); @@ -804,7 +785,7 @@ pub fn native_clear_osc_config() -> Result<(), backend::VibeCheckFSError> { } } } - return Ok(()); + Ok(()) } pub fn native_simulate_device_feature( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 15fd7b7..999ad30 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "VibeCheck", - "version": "0.3.2" + "version": "0.4.0" }, "tauri": { "systemTray": { @@ -74,8 +74,8 @@ "title": "VibeCheck", "width": 900, "height": 600, - "minWidth": 600, - "minHeight": 600 + "minWidth": 620, + "minHeight": 400 } ] } diff --git a/src/App.css b/src/App.css index 6764bfc..52e1bd6 100644 --- a/src/App.css +++ b/src/App.css @@ -27,10 +27,6 @@ } .scrollbar::-webkit-scrollbar-thumb { - background: #3f3f46; - border-radius: 100vh; -} - -.scrollbar::-webkit-scrollbar-thumb:hover { background: #4b5563; + border-radius: 100vh; } diff --git a/src/App.tsx b/src/App.tsx index 397b851..dc8dcde 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,9 +11,10 @@ import VrchatLogo from "./assets/vrchat-192x192.png"; import ExternalLogo from "./components/ExternalLogo"; import Loading from "./components/Loading"; import UpdatePing from "./components/UpdatePing"; +import { useCoreEventContext } from "./context/CoreEvents"; +import { TOOLTIP } from "./data/constants"; import Config from "./features/Config"; import Toy from "./features/Toy"; -import { useCoreEvents } from "./hooks/useCoreEvents"; import { parseName, toyKey, useToys } from "./hooks/useToys"; import { useUpdate } from "./hooks/useUpdate"; import { useVersion } from "./hooks/useVersion"; @@ -48,7 +49,7 @@ export default function App() { toggleIsEnabled, config, refreshConfig, - } = useCoreEvents(); + } = useCoreEventContext(); const { canUpdate } = useUpdate(); const { version } = useVersion(); @@ -104,21 +105,9 @@ export default function App() {
- - - + + +
@@ -164,7 +153,7 @@ export default function App() { /> ; }; -export default function ExternalLogo({ - src, - link, - tooltip, -}: ExternalLogoProps) { +export default function ExternalLogo({ src, tooltip }: ExternalLogoProps) { async function openBrowser() { try { - await invoke(OPEN_BROWSER, { link: link }); + await invoke(INVOKE.OPEN_BROWSER, { link: tooltip.link }); } catch (e) { createToast("error", "Could not open browser", JSON.stringify(e)); } } return ( - + ); diff --git a/src/components/FourPanel.tsx b/src/components/FourPanel.tsx index d9db2b5..1e508b8 100644 --- a/src/components/FourPanel.tsx +++ b/src/components/FourPanel.tsx @@ -1,3 +1,5 @@ +import { TOOLTIP } from "@/data/constants"; +import { ObjectValues } from "@/utils"; import { ArrowRightLeft } from "lucide-react"; import { ReactNode } from "react"; import { TooltipLabel } from "../layout/Tooltip"; @@ -11,7 +13,7 @@ export default function FourPanel({ four, }: { text: string; - tooltip?: string; + tooltip?: string | ObjectValues; flipped?: boolean; two?: ReactNode; three?: ReactNode; @@ -32,8 +34,8 @@ export default function FourPanel({ ) : ( label )} -
{two}
-
+
{two}
+
{three}
{four}
diff --git a/src/components/FourPanelContainer.tsx b/src/components/FourPanelContainer.tsx index 0dd5601..9002da9 100644 --- a/src/components/FourPanelContainer.tsx +++ b/src/components/FourPanelContainer.tsx @@ -6,7 +6,7 @@ export default function FourPanelContainer({ children: ReactNode; }) { return ( -
+
{children}
); diff --git a/src/hooks/useCoreEvents.tsx b/src/context/CoreEvents.tsx similarity index 60% rename from src/hooks/useCoreEvents.tsx rename to src/context/CoreEvents.tsx index f0c8209..05f28db 100644 --- a/src/hooks/useCoreEvents.tsx +++ b/src/context/CoreEvents.tsx @@ -1,29 +1,51 @@ import { invoke } from "@tauri-apps/api"; import { listen } from "@tauri-apps/api/event"; -import { useEffect, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import type { FeCoreEvent } from "../../src-tauri/bindings/FeCoreEvent"; import { FeStateEvent } from "../../src-tauri/bindings/FeStateEvent"; import type { FeVibeCheckConfig } from "../../src-tauri/bindings/FeVibeCheckConfig"; import { createToast } from "../components/Toast"; -import { - CORE_EVENT, - DISABLE, - ENABLE, - GET_CONFIG, - SCAN_LENGTH, - START_SCAN, - STOP_SCAN, -} from "../data/constants"; +import { INVOKE, LISTEN } from "../data/constants"; import { assertExhaustive } from "../utils"; -export function useCoreEvents() { +const SCAN_LENGTH = 10000; + +type CoreEventContextProps = { + isScanning: boolean; + isEnabled: boolean; + toggleIsEnabled: () => Promise; + toggleScan: () => Promise; + config: FeVibeCheckConfig | undefined; + refreshConfig: () => Promise; +}; + +const CoreEventContext = createContext({ + isScanning: false, + isEnabled: false, + toggleIsEnabled: () => new Promise(() => null), + toggleScan: () => new Promise(() => null), + config: undefined, + refreshConfig: () => new Promise(() => null), +}); + +export function useCoreEventContext() { + const context = useContext(CoreEventContext); + if (!context) { + throw new Error("useCoreEventContext not within context provider"); + } + return context; +} + +export function CoreEventProvider({ children }: { children: React.ReactNode }) { const [isEnabled, setIsEnabled] = useState(false); const [isScanning, setIsScanning] = useState(false); - const [config, setConfig] = useState(null); + const [config, setConfig] = useState( + undefined, + ); async function enable() { try { - await invoke(ENABLE); + await invoke(INVOKE.ENABLE); setIsEnabled(true); } catch (e) { createToast("error", "Could not enable!", JSON.stringify(e)); @@ -33,7 +55,7 @@ export function useCoreEvents() { async function stopScanAndDisable() { try { await stopScan(); - await invoke(DISABLE); + await invoke(INVOKE.DISABLE); setIsEnabled(false); } catch (e) { createToast("error", "Could not disable!", JSON.stringify(e)); @@ -51,7 +73,7 @@ export function useCoreEvents() { async function enableAndStartScan() { try { await enable(); - await invoke(START_SCAN); + await invoke(INVOKE.START_SCAN); setIsScanning(true); } catch (e) { createToast("error", "Could not start scan!", JSON.stringify(e)); @@ -60,7 +82,7 @@ export function useCoreEvents() { async function stopScan() { try { - await invoke(STOP_SCAN); + await invoke(INVOKE.STOP_SCAN); setIsScanning(false); } catch (e) { createToast("error", "Could not stop scan!", JSON.stringify(e)); @@ -108,7 +130,7 @@ export function useCoreEvents() { } useEffect(() => { - const unlistenPromise = listen(CORE_EVENT, (event) => + const unlistenPromise = listen(LISTEN.CORE_EVENT, (event) => handleCoreEvent(event.payload), ); @@ -119,31 +141,37 @@ export function useCoreEvents() { async function refreshConfig() { try { - const config = await invoke(GET_CONFIG); + const config = await invoke(INVOKE.GET_CONFIG); setConfig(config); } catch { - setConfig(null); + setConfig(undefined); } } useEffect(() => { async function getConfig() { try { - const config = await invoke(GET_CONFIG); + const config = await invoke(INVOKE.GET_CONFIG); setConfig(config); } catch { - setConfig(null); + setConfig(undefined); } } getConfig(); }, []); - return { - isScanning, - isEnabled, - toggleIsEnabled, - toggleScan, - config, - refreshConfig, - }; + return ( + + {children} + + ); } diff --git a/src/data/constants.ts b/src/data/constants.ts index 65626d8..2e121d8 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -1,20 +1,123 @@ -export const CORE_EVENT = "fe_core_event"; -export const TOY_EVENT = "fe_toy_event"; -export const ALTER_TOY = "alter_toy"; -export const CLEAR_OSC_CONFIG = "clear_osc_config"; -export const SIMULATE_TOY_FEATURE = "simulate_device_feature"; +export const LISTEN = { + CORE_EVENT: "fe_core_event", + TOY_EVENT: "fe_toy_event", +} as const; -export const VERSION = "vibecheck_version"; -export const START_SCAN = "vibecheck_start_bt_scan"; -export const STOP_SCAN = "vibecheck_stop_bt_scan"; -export const ENABLE = "vibecheck_enable"; -export const DISABLE = "vibecheck_disable"; -export const GET_CONFIG = "get_vibecheck_config"; -export const SET_CONFIG = "set_vibecheck_config"; -export const OPEN_BROWSER = "open_default_browser"; -export const OFFLINE_SYNC = "sync_offline_toys"; +export const INVOKE = { + ALTER_TOY: "alter_toy", + CLEAR_OSC_CONFIG: "clear_osc_config", + SIMULATE_TOY_FEATURE: "simulate_device_feature", + VERSION: "vibecheck_version", + START_SCAN: "vibecheck_start_bt_scan", + STOP_SCAN: "vibecheck_stop_bt_scan", + ENABLE: "vibecheck_enable", + DISABLE: "vibecheck_disable", + GET_CONFIG: "get_vibecheck_config", + SET_CONFIG: "set_vibecheck_config", + OPEN_BROWSER: "open_default_browser", + OFFLINE_SYNC: "sync_offline_toys", +} as const; -export const SCAN_LENGTH = 10000; +export const OSC = { + PARAM_PREFIX: "/avatar/parameters/", + DATA_PREFIX: "vibecheck/osc_data/", +} as const; -export const OSC_PARAM_PREFIX = "/avatar/parameters/"; -export const OSC_DATA_PREFIX = "vibecheck/osc_data/"; +export const TOOLTIP = { + VrChatGroup: { + text: "Vibecheck VRChat Group", + link: "VRChatGroup", + }, + Discord: { + text: "Vibecheck Discord", + link: "Discord", + }, + Github: { + text: "Vibecheck Github", + link: "Github", + }, + Enabled: { + text: "Enable/Disable this feature.", + link: "", + }, + OSC_Data: { + text: "If vibecheck should send OSC data to VRChat", + link: "ToyOptions", + }, + Anatomy: { + text: "Anatomy types can be used as a category filter to disable/enable multiple toys at the same time from VRChat using the VibeCheck OSC API", + link: "ToyOptions", + }, + InputProcessor: { + text: "Choose the way VibeCheck processes input. Example: If your avatar is using SPS and you want VibeCheck to interact with it, switch to SPS.", + link: "FeatureOptions", + }, + LinearSpeed: { + text: "Linear positional duration speed in milliseconds. Speed is determined by the toy itself, this is only requested speed.", + link: "FeatureOptions", + }, + FlipInput: { + text: "Some toys use a flipped float input. Enable this if your toy seems to do the opposite motor level you were expecting.", + link: "FeatureOptions", + }, + Idle: { + text: "Set the idle motor speed for this feature. Idle activates when there is no input. Your set idle speed won't activate until you send at least one float value in the valid min/max range you have set.", + link: "FeatureOptions", + }, + Range: { + text: "The minimum/maximum motor speed that will be sent to the feature's motor.", + link: "FeatureOptions", + }, + Smooth: { + text: "This smooths the float input by queueing the amount set with the slider, then transforming them into one value to send instead. If you aren't sending a lot of floats rapidly over OSC you probably want this disabled completely.", + link: "", + }, + Rate: { + text: "This uses rate mode on the float input.", + link: "", + }, + Constant: { + text: "The intensity your toy will activate when you have constant mode enabled.", + link: "", + }, + Simulate: { + text: "Test feature power level.", + link: "FeatureOptions", + }, + OSC_Bind: { + text: "OSC Receive Port (Default: 127.0.0.1:9001)", + link: "", + }, + OSC_Remote: { + text: "OSC Receive Port (Default: 127.0.0.1:9001)", + link: "", + }, + OSC_Send: { + text: "OSC Send Port (Default: 127.0.0.1:9000)", + link: "", + }, + LC_Override: { + text: "Override and force the Lovense Connect host to connect to.", + link: "", + }, + ScanOnDisconnect: { + text: "Automatically start scanning when a toy disconnects.", + link: "", + }, + MinimizeOnExit: { + text: "Minimize VibeCheck instead of exiting.", + link: "", + }, + DesktopNotifications: { + text: "Notifications for toy connect and disconnect.", + link: "", + }, + AdvancedToy: { + text: "Show advanced toy options like osc data and anatomy", + link: "", + }, + AdvancedFeature: { + text: "Show advanced options for features [vibrator, constrict, oscillate, etc], will show options like idle speed, flip input, simulate", + link: "", + }, +} as const; diff --git a/src/features/Config.tsx b/src/features/Config.tsx index c0f986d..46a49dc 100644 --- a/src/features/Config.tsx +++ b/src/features/Config.tsx @@ -3,7 +3,7 @@ import { ChangeEvent, FormEvent, useState } from "react"; import type { FeVibeCheckConfig } from "../../src-tauri/bindings/FeVibeCheckConfig"; import { createToast } from "../components/Toast"; import UpdateButton from "../components/UpdateButton"; -import { CLEAR_OSC_CONFIG, SET_CONFIG } from "../data/constants"; +import { INVOKE, TOOLTIP } from "../data/constants"; import Button from "../layout/Button"; import Switch from "../layout/Switch"; import { TooltipLabel } from "../layout/Tooltip"; @@ -53,7 +53,7 @@ export default function Config({ ) { await disableOnPortChange(); } - await invoke(SET_CONFIG, { feVcConfig: newConfig }); + await invoke(INVOKE.SET_CONFIG, { feVcConfig: newConfig }); createToast("info", "Saved config"); } catch (e) { createToast("error", "Could not set config!", JSON.stringify(e)); @@ -62,7 +62,7 @@ export default function Config({ async function clearOsc() { try { - await invoke(CLEAR_OSC_CONFIG); + await invoke(INVOKE.CLEAR_OSC_CONFIG); createToast( "info", "Cleared avatar OSC configs", @@ -91,10 +91,7 @@ export default function Config({
- +
- +
+ + + onCheckSwitch(checked, "show_toy_advanced") + } + size="small" + /> +
+ + + onCheckSwitch(checked, "show_feature_advanced") + } + size="small" + /> +
diff --git a/src/features/FeatureForm.tsx b/src/features/FeatureForm.tsx index 672cb80..b28b0f7 100644 --- a/src/features/FeatureForm.tsx +++ b/src/features/FeatureForm.tsx @@ -1,9 +1,21 @@ -import { Button } from "@/components/ui/button"; +import { useCoreEventContext } from "@/context/CoreEvents"; import { PenetrationSystems, ProcessingModes } from "@/data/stringArrayTypes"; import { Select } from "@/layout/Select"; +import { cn } from "@/lib/utils"; import { ScrollArea } from "@radix-ui/react-scroll-area"; +import { DebouncedFunc, debounce } from "lodash"; import { Plus, X } from "lucide-react"; -import { ChangeEvent, Fragment, ReactNode, useEffect, useState } from "react"; +import { + ChangeEvent, + Dispatch, + Fragment, + ReactNode, + SetStateAction, + createContext, + useCallback, + useContext, + useState, +} from "react"; import { FeProcessingMode } from "src-tauri/bindings/FeProcessingMode"; import { FeToyParameter } from "src-tauri/bindings/FeToyParameter"; import { FeLevelTweaks } from "../../src-tauri/bindings/FeLevelTweaks"; @@ -11,13 +23,39 @@ import { FeVCToy } from "../../src-tauri/bindings/FeVCToy"; import type { FeVCToyFeature } from "../../src-tauri/bindings/FeVCToyFeature"; import FourPanel from "../components/FourPanel"; import FourPanelContainer from "../components/FourPanelContainer"; -import { OSC_PARAM_PREFIX } from "../data/constants"; +import { OSC, TOOLTIP } from "../data/constants"; import useSimulate from "../hooks/useSimulate"; -import { handleFeatureAlter } from "../hooks/useToys"; +import { handleFeatureAlter as handleToyFeatureAlter } from "../hooks/useToys"; import Slider from "../layout/Slider"; import Switch from "../layout/Switch"; import { round0 } from "../utils"; +type FeatureFormContextProps = { + feature: FeVCToyFeature; + setToyFeature: Dispatch>; + debouncedAlter: DebouncedFunc<(f: FeVCToyFeature) => void>; + handleFeatureAlter: (f: FeVCToyFeature) => void; + handleBool: (checked: boolean, name: keyof FeVCToyFeature) => void; + handleLevels: (key: keyof FeLevelTweaks, value: number) => void; +}; + +const FeatureFormContext = createContext({ + feature: {} as FeVCToyFeature, + setToyFeature: () => null, + debouncedAlter: debounce(() => null, 1000), + handleFeatureAlter: () => null, + handleBool: () => null, + handleLevels: () => null, +}); + +const useFeatureFormContext = () => { + const context = useContext(FeatureFormContext); + if (!context) { + throw new Error("useFeatureFormContext not within context provider"); + } + return context; +}; + type ToyFeatureFormProps = { toy: FeVCToy; selectedIndex: number; @@ -31,30 +69,151 @@ export default function FeatureForm({ toy.features[selectedIndex] ?? toy.features[0], ); const levels = feature.feature_levels; - const submenuOptions = ["Parameters", "Advanced"] as const; - type SubmenuOptions = (typeof submenuOptions)[number]; - const [subMenu, setSubMenu] = useState("Parameters"); + const { config } = useCoreEventContext(); - useEffect(() => { - setToyFeature(toy.features[selectedIndex] ?? toy.features[0]); - }, [toy, selectedIndex]); + // Only need debounce for input fields, levels work with onValueCommit + // Fast debounce because otherwise we'd have to merge with other updates + const debouncedAlter = useCallback( + debounce((f) => handleFeatureAlter(f), 100), + [], + ); - const { - simulateEnabled, - simulateLevel, - toggleSimulate, - simulateOnValueChange, - simulateOnValueCommit, - } = useSimulate(toy.toy_id, feature); + function handleFeatureAlter(feature: FeVCToyFeature) { + handleToyFeatureAlter(toy, feature); + } function handleBool(checked: boolean, name: keyof FeVCToyFeature) { setToyFeature((f) => { const newF = { ...f, [name]: checked } as FeVCToyFeature; - handleFeatureAlter(toy, newF); + handleFeatureAlter(newF); + return newF; + }); + } + + function handleLevels(key: keyof FeLevelTweaks, value: number) { + setToyFeature((feature) => { + return { + ...feature, + feature_levels: { ...levels, [key]: value }, + }; + }); + } + + const tweakSliders = feature.osc_parameters.reduce( + (seenModes, oscParam) => { + seenModes.add(oscParam.processing_mode); + return seenModes; + }, + new Set([feature.penetration_system.pen_system_processing_mode]), + ); + + return ( + +
+ + + + + + {config?.show_feature_advanced && ( + <> + + + {feature.feature_type == "Linear" && } + {tweakSliders.has("Smooth") && } + {tweakSliders.has("Rate") && } + {tweakSliders.has("Constant") && } + + + )} + +
Parameters
+ + + +
+
+
+ ); +} + +function Enabled() { + const { feature, handleBool } = useFeatureFormContext(); + + return ( + handleBool(checked, "feature_enabled")} + /> + } + /> + ); +} + +function InputProcessor() { + const { feature, setToyFeature, handleFeatureAlter } = + useFeatureFormContext(); + + function handleInputProcessor(e: ChangeEvent) { + setToyFeature((f) => { + const newF = { + ...f, + penetration_system: { + ...f.penetration_system, + [e.target.name]: e.target.value, + }, + }; + handleFeatureAlter(newF); return newF; }); } + return ( + + { + handleInputProcessor(e); + }} + options={ProcessingModes} + /> +
+ } + /> + ); +} + +function Parameters() { + const { feature, setToyFeature, handleFeatureAlter, debouncedAlter } = + useFeatureFormContext(); + function removeParam(parameter: string) { setToyFeature((f) => { const newF = { @@ -63,7 +222,7 @@ export default function FeatureForm({ (param) => param.parameter != parameter, ), }; - handleFeatureAlter(toy, newF); + handleFeatureAlter(newF); return newF; }); } @@ -82,7 +241,7 @@ export default function FeatureForm({ function addParam() { setToyFeature((f) => { - const newParam = `${OSC_PARAM_PREFIX}param-${findParamName( + const newParam = `${OSC.PARAM_PREFIX}param-${findParamName( f.osc_parameters, )}`; const newF = { @@ -91,344 +250,316 @@ export default function FeatureForm({ ...f.osc_parameters, { parameter: newParam, - processing_mode: "Raw" as FeProcessingMode, + processing_mode: "Raw" as const, }, ], }; - handleFeatureAlter(toy, newF); + handleFeatureAlter(newF); return newF; }); } function handleOscParam( - e: ChangeEvent | ChangeEvent, + e: ChangeEvent, paramIndex: number, ) { setToyFeature((f) => { const newParams = [...f.osc_parameters]; - if (e.target.name == "osc_parameter") { - newParams[paramIndex].parameter = - `${OSC_PARAM_PREFIX}${e.target.value}`; - } else if (e.target.name == "osc_parameter_mode") { - newParams[paramIndex].processing_mode = e.target - .value as FeProcessingMode; - } + newParams[paramIndex].parameter = normalizeOscParameter(e.target.value); const newF = { ...f, osc_parameters: newParams, }; - handleFeatureAlter(toy, newF); + debouncedAlter(newF); return newF; }); } - function handleInputProcessor(e: ChangeEvent) { + function handleOscParamMode( + e: ChangeEvent, + paramIndex: number, + ) { setToyFeature((f) => { + const newParams = [...f.osc_parameters]; + newParams[paramIndex].processing_mode = e.target + .value as FeProcessingMode; const newF = { ...f, - penetration_system: { - ...f.penetration_system, - [e.target.name]: e.target.value, - }, + osc_parameters: newParams, }; - handleFeatureAlter(toy, newF); + handleFeatureAlter(newF); return newF; }); } - function handleLevels(key: keyof FeLevelTweaks, value: number) { - setToyFeature((feature) => { - return { - ...feature, - feature_levels: { ...levels, [key]: value }, - }; - }); + function normalizeOscParameter(p: string) { + return `${OSC.PARAM_PREFIX}${p.replaceAll(" ", "_")}`; } - function handleCommit() { - handleFeatureAlter(toy, feature); - } + return ( + <> + {/*
*/} + {feature.osc_parameters.map((param, paramIndex) => { + // TODO: Using index is generally an anti-pattern, but I think it's required in this specific scenario + // If we key on a parameter or other identifiers, typing the parameter name would trigger a refresh from the backend + // This would then deselect the input element while typing + return ( + + {/* Adding debounce on this makes it more complex b/c separate state, plus parent key on index */} + handleOscParam(e, paramIndex)} + /> + handleOscParam(e, paramIndex)} - /> - { - handleInputProcessor(e); - }} - options={PenetrationSystems} - /> -