//! Common code between all controllers. //! //! Mostly the boring stuff, e.g. thread and state management. #![allow(clippy::missing_safety_doc)] // Make it public so Rust shuts up about unused backends pub mod backends; mod configuration; mod log; use std::{ ffi::c_void, fmt::Display, rc::Rc, sync::atomic::{AtomicBool, Ordering}, time::Duration, }; use ::log::error; use anyhow::{anyhow, Result}; use shared_memory::{Shmem, ShmemConf, ShmemError}; use winapi::{ shared::{ minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE}, winerror::{E_FAIL, S_OK}, }, um::winnt::{DLL_PROCESS_ATTACH, HRESULT}, }; use crate::log::init_logger; cfg_if::cfg_if! { if #[cfg(feature = "tasoller_v1")] { use crate::backends::tasoller_v1 as con_impl; } else if #[cfg(feature = "tasoller_v2")] { use crate::backends::tasoller_v2 as con_impl; } else if #[cfg(feature = "laverita_v2")] { use crate::backends::laverita_v2 as con_impl; } else if #[cfg(feature = "redboard")] { use crate::backends::redboard as con_impl; } else { use crate::backends::dummy as con_impl; } } cfg_if::cfg_if! { if #[cfg(any(chuni, chusanapp))] { type SliderCallbackFn = unsafe extern "C" fn(data: *const u8); use std::thread; use ::log::info; use rusb::{DeviceHandle, GlobalContext}; use parking_lot::RwLock; use crate::backends::ReadType; static DEVICE: RwLock>> = RwLock::new(None); static SLIDER_ACTIVE: AtomicBool = AtomicBool::new(false); static SLIDER_OUTPUT: RwLock<[u8; con_impl::OUTPUT_MEMORY_SIZE]> = RwLock::new([0u8; con_impl::OUTPUT_MEMORY_SIZE]); } } cfg_if::cfg_if! { if #[cfg(any(chuni, amdaemon))] { use std::{ffi::{c_int, CString}, sync::atomic::AtomicU16}; use lazy_static::lazy_static; use winapi::um::{winbase::GetPrivateProfileIntA, winuser::GetAsyncKeyState}; use crate::configuration::Configuration; static COIN_COUNT: AtomicU16 = AtomicU16::new(0); static COIN_PRESSED: AtomicBool = AtomicBool::new(false); lazy_static! { static ref CONFIGURATION: Configuration = { let io3 = CString::new("io3").unwrap(); let test = CString::new("test").unwrap(); let service = CString::new("service").unwrap(); let coin = CString::new("coin").unwrap(); let cfg_file = CString::new(".\\segatools.ini").unwrap(); unsafe { Configuration { test_key: GetPrivateProfileIntA( io3.as_ptr(), test.as_ptr(), 0x31, cfg_file.as_ptr(), ), service_key: GetPrivateProfileIntA( io3.as_ptr(), service.as_ptr(), 0x32, cfg_file.as_ptr(), ), coin_key: GetPrivateProfileIntA( io3.as_ptr(), coin.as_ptr(), 0x33, cfg_file.as_ptr(), ), } } }; } } } pub static TIMEOUT: Duration = Duration::from_millis(20); pub static DEVICE_POLLING_INTERVAL: Duration = Duration::from_millis(100); static mut INPUT_SHMEM: Option> = None; const INPUT_SHMEM_OS_ID: &str = "fcfe5b1100568d65af167d81acbed71d"; #[no_mangle] extern "system" fn DllMain(_dll_module: HINSTANCE, call_reason: DWORD, _reserved: LPVOID) -> BOOL { if call_reason != DLL_PROCESS_ATTACH { return TRUE; } init_logger(); TRUE } #[no_mangle] pub extern "C" fn chuni_io_get_api_version() -> u16 { 0x0102 } #[no_mangle] #[cfg(any(chuni, amdaemon))] pub extern "C" fn chuni_io_jvs_init() -> HRESULT { #[cfg(chuni)] { // We don't care if we can't find the device at this time, it'll // probably come eventually. thread::spawn(device_init); } create_input_shared_memory() } #[no_mangle] #[cfg(chusanapp)] pub extern "C" fn chuni_io_jvs_init() -> HRESULT { S_OK } #[no_mangle] #[cfg(any(chuni, amdaemon))] pub unsafe extern "C" fn chuni_io_jvs_poll(opbtn: *mut u8, beams: *mut u8) { if opbtn.is_null() || beams.is_null() { return; } let Some(input_shmem) = &INPUT_SHMEM else { return; }; (*opbtn, *beams) = con_impl::jvs_poll(input_shmem.as_slice()); if GetAsyncKeyState(CONFIGURATION.test_key as c_int) != 0 { *opbtn |= 1; } if GetAsyncKeyState(CONFIGURATION.service_key as c_int) != 0 { *opbtn |= 2; } } #[no_mangle] #[cfg(chusanapp)] pub extern "C" fn chuni_io_jvs_poll(_opbtn: *mut u8, _beams: *mut u8) {} #[no_mangle] #[cfg(any(chuni, amdaemon))] pub unsafe extern "C" fn chuni_io_jvs_read_coin_counter(total: *mut u16) { let Some(input_shmem) = &INPUT_SHMEM else { return; }; if con_impl::is_coin_button_pressed(input_shmem.as_slice()) || GetAsyncKeyState(CONFIGURATION.coin_key as c_int) != 0 { if !COIN_PRESSED.load(Ordering::Relaxed) { COIN_PRESSED.store(true, Ordering::Relaxed); COIN_COUNT.fetch_add(1, Ordering::Relaxed); } } else { COIN_PRESSED.store(false, Ordering::Relaxed); } *total = COIN_COUNT.load(Ordering::Relaxed); } #[no_mangle] #[cfg(chusanapp)] pub extern "C" fn chuni_io_jvs_read_coin_counter(_total: *mut u16) {} #[no_mangle] #[cfg(any(chuni, chusanapp))] #[allow(unreachable_code)] pub unsafe extern "C" fn chuni_io_slider_init() -> HRESULT { #[cfg(not(feature = "chusan"))] { // Already initialized in chuni_io_jvs_init() return S_OK; } // We don't care if we can't find the device at this time, it'll // probably come eventually. thread::spawn(device_init); create_input_shared_memory() } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_slider_init() -> HRESULT { S_OK } #[no_mangle] #[cfg(any(chuni, chusanapp))] pub unsafe extern "C" fn chuni_io_slider_start(callback: *const c_void) { if callback.is_null() { return; } if SLIDER_ACTIVE.load(Ordering::SeqCst) { return; } SLIDER_ACTIVE.store(true, Ordering::SeqCst); let callback = std::mem::transmute::<_, SliderCallbackFn>(callback); thread::spawn(move || { let Some(input_shmem) = (unsafe { &INPUT_SHMEM }) else { return; }; let usb_in = input_shmem.as_slice(); while SLIDER_ACTIVE.load(Ordering::SeqCst) { let pressure = con_impl::read_pressure_data(usb_in); callback(pressure.as_ptr()); thread::sleep(Duration::from_nanos(1_000_000)); } }); } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_slider_start(_callback: *const c_void) {} #[no_mangle] #[cfg(any(chuni, chusanapp))] pub extern "C" fn chuni_io_slider_stop() { SLIDER_ACTIVE.store(false, Ordering::SeqCst); } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_slider_stop() {} #[no_mangle] #[cfg(any(chuni, chusanapp))] pub unsafe extern "C" fn chuni_io_slider_set_leds(rgb: *const u8) { if rgb.is_null() { return; } let device_rg = DEVICE.read(); let Some(device) = device_rg.as_ref().map(|z| z) else { return }; let rgb = std::slice::from_raw_parts(rgb, 93); let mut output = SLIDER_OUTPUT.write(); if let Err(e) = con_impl::set_slider_leds(device, output.as_mut_slice(), rgb) { error!("Could not write slider LED data: {e:#?}"); } } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_slider_set_leds(_rgb: *const u8) {} #[no_mangle] #[cfg(any(chuni, chusanapp))] pub extern "C" fn chuni_io_led_init() -> HRESULT { let mut output = SLIDER_OUTPUT.write(); con_impl::init_output_buffer(output.as_mut_slice()); S_OK } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_led_init() -> HRESULT { S_OK } #[no_mangle] #[cfg(any(chuni, chusanapp))] pub unsafe extern "C" fn chuni_io_led_set_colors(board: u8, rgb: *const u8) { if rgb.is_null() { return; } let device_rg = DEVICE.read(); let Some(device) = device_rg.as_ref().map(|z| z) else { return }; let rgb = std::slice::from_raw_parts(rgb, 256); let mut output = SLIDER_OUTPUT.write(); if let Err(e) = con_impl::set_led_colors(device, output.as_mut_slice(), board, rgb) { error!("Could not write RGB LED data: {e:#?}"); } } #[no_mangle] #[cfg(amdaemon)] pub extern "C" fn chuni_io_led_set_colors(_rgb: *const u8) {} #[cfg(any(chuni, chusanapp))] fn device_init() -> Result<()> { { let mut global_device = DEVICE.write(); *global_device = None; } info!("Waiting for device..."); loop { let Some(mut device) = rusb::open_device_with_vid_pid(con_impl::DEVICE_VID, con_impl::DEVICE_PID) else { thread::sleep(DEVICE_POLLING_INTERVAL); continue; }; device.set_active_configuration(1)?; device.claim_interface(0)?; let mut global_device = DEVICE.write(); *global_device = Some(device); thread::spawn(input_thread_proc); return Ok(()) } } #[cfg(any(chuni, chusanapp))] fn input_thread_proc() { info!("Input thread started"); let mut shmem = match create_shared_memory(INPUT_SHMEM_OS_ID, con_impl::INPUT_MEMORY_SIZE, false) { Ok(s) => s, Err(e) => { error!("Could not obtain shared memory for controller input: {e:#?}"); return; } }; let usb_in = unsafe { shmem.as_slice_mut() }; let device_rg = DEVICE.read(); let device = device_rg.as_ref().unwrap(); loop { let result = match con_impl::READ_TYPE { ReadType::Bulk => device.read_bulk(con_impl::READ_ENDPOINT, usb_in, TIMEOUT), ReadType::Interrupt => device.read_interrupt(con_impl::READ_ENDPOINT, usb_in, TIMEOUT), }; if let Err(e) = result { match e { rusb::Error::NoDevice | rusb::Error::Io => { error!("Controller disconnected."); usb_in.iter_mut().for_each(|m| *m = 0); // Spawn a thread polling for a connection again thread::spawn(device_init); return; } _ => error!("Could not read data from controller: {e:#?}"), } } } } fn create_input_shared_memory() -> HRESULT { match create_shared_memory(INPUT_SHMEM_OS_ID, con_impl::INPUT_MEMORY_SIZE, false) { Ok(mut s) => { unsafe { s.as_slice_mut().iter_mut().for_each(|m| *m = 0); INPUT_SHMEM = Some(Rc::new(s)) }; S_OK } Err(e) => { error!("Could not acquire shared memory: {e:#?}"); E_FAIL } } } fn create_shared_memory(os_id: S, size: usize, is_owner: bool) -> Result where S: AsRef + Display + Copy, { let shmem_conf = ShmemConf::new().size(size).os_id(os_id); let shmem_result = match shmem_conf.clone().create() { Ok(s) => Ok(s), Err(ShmemError::MappingIdExists) => shmem_conf.open(), Err(e) => { return Err(anyhow!( "Failed to create/open shared memory {os_id}: {e:#}" )); } }; shmem_result .map(|mut m| { m.set_owner(is_owner); m }) .map_err(|e| anyhow!("Failed to create/open shared memory {os_id}: {e:#}")) }