use std::sync::OnceLock; use tokio::net::TcpListener; use tokio::runtime::Runtime; use tracing::{info, warn}; mod config; mod card; mod webapi; mod metrics; mod utils; mod webserver; use card::CardPresenter; use config::Config; use metrics::SystemMetrics; // Re-export public API for library users pub use config::Config as ServerConfig; pub use card::{CardPresenter as CardSlot, StoredCard, CardPresentResult}; pub use metrics::SystemMetrics as Metrics; pub use webserver::build_server; pub use utils::{format_hex_bytes, parse_access_code, parse_idm_hex}; static WEBSERVER_INIT: OnceLock<()> = OnceLock::<()>::new(); #[unsafe(no_mangle)] pub extern "C" fn aime_io_get_api_version() -> u16 { 0x0100 } #[unsafe(no_mangle)] pub extern "C" fn aime_io_init() -> i32 { WEBSERVER_INIT.get_or_init(|| { std::thread::spawn(|| { let rt = Runtime::new().expect("Failed to create tokio runtime"); rt.block_on(start_server_async()); }); }); return 0; } #[unsafe(no_mangle)] pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> i32 { if unit_no != 0 { return 1; } if let Err(_) = SystemMetrics::instance().set_last_poll() { warn!("Failed to update last poll time"); return 1; } #[cfg(target_os = "windows")] { use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState; let config = Config::instance(); if !config.enable_aime_txt { return 1; } let enter_pressed = unsafe { (GetAsyncKeyState(0x0D) & (0x8000u16 as i16)) != 0 }; if !enter_pressed { return 1; } let aime_code = match std::fs::read_to_string(&config.aime_txt_path) { Ok(content) => content.trim().to_string(), Err(e) => { warn!("Failed to read text file at {}: {}", config.aime_txt_path.display(), e); return 1; } }; if aime_code.is_empty() { warn!("Access code file is empty"); return 1; } let Some(access_code) = utils::parse_access_code(&aime_code) else { warn!("Invalid access code read from file"); return 1; }; match CardPresenter::instance().present_card(access_code, None, std::time::Duration::from_secs(5)) { card::CardPresentResult::Accepted => { return 0; }, card::CardPresentResult::SlotUnavailable(release_time) => { warn!("Access code was not accepted as there is already a card in the active slot until {:?}", release_time); } } } return 1; } #[unsafe(no_mangle)] pub extern "C" fn aime_io_nfc_get_aime_id(unit_no: u8, luid: *mut u8, luid_size: usize) -> i32 { if unit_no != 0 { return 1; } let require_idm = if Config::instance().physical_card_use_idm { Some(false) } else { None }; let Some(card) = CardPresenter::instance().request_card(require_idm) else { return 1; }; unsafe { let copy_size = std::cmp::min(card.access_code.len(), luid_size); std::ptr::copy_nonoverlapping(card.access_code.as_ptr(), luid, copy_size); } info!("Provided access code: {}", utils::format_hex_bytes(&card.access_code)); return 0; } #[unsafe(no_mangle)] pub extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> i32 { if unit_no != 0 || Config::instance().physical_card_use_idm == false { return 1; } let Some(card) = CardPresenter::instance().request_card(Some(true)) else { return 1; }; let Some(card_idm) = card.card_idm else { return 1; }; let mut idm_value: u64 = 0; for byte in card_idm { idm_value = (idm_value << 8) | (byte as u64); } unsafe { *idm = idm_value; } info!("Provided IDm: {}", utils::format_hex_bytes(&card_idm)); return 0; } #[unsafe(no_mangle)] pub extern "C" fn aime_io_led_set_color(_unit_no: u8, _r: u8, _g: u8, _b: u8) { // do nothing return; } #[unsafe(no_mangle)] #[allow(non_snake_case)] #[cfg(target_os = "windows")] pub extern "system" fn DllMain(_hinst_dll: *mut std::ffi::c_void, fdw_reason: u32, _lpv_reserved: *mut std::ffi::c_void) -> i32 { if fdw_reason != 1 /* DLL_PROCESS_ATTACH */ { return 1; } unsafe { windows_sys::Win32::System::Console::AllocConsole(); } tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .with_ansi(false) // some targets that are injected into don't play nicely with ansi codes .init(); return 1; } async fn start_server_async() { let app = webserver::build_server(); let config = Config::instance(); let listen_addr = config.server_listen_address.as_str(); let listener = TcpListener::bind(listen_addr) .await .unwrap_or_else(|e| panic!("Failed to bind to {}: {}", listen_addr, e)); SystemMetrics::instance().set_start_time(); let game_id = config.game_id.clone().unwrap_or_else(|| "SXXX".to_string()); info!("AMNet Server - Version {} ({}) loaded", env!("CARGO_PKG_VERSION"), game_id); info!("Listening on {} - visit http://card.ppc.moe from a mobile device or use the AMNet App to get started.", listen_addr); axum::serve(listener, app) .await .expect("Failed to start server"); }