From 453a7961876d8e46e1375f5afcfc65ab8bb91751 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 5 Feb 2024 15:58:48 -0800 Subject: [PATCH 1/4] Initial port of SlippiDirectCodes to Rust. This moves the direct codes parsing/loading/vending from C++ to Rust. Where applicable, certain things that weren't in use in the C++ version - but may be useful in the future - were also ported but commented out. The only significant change here is that this changes the `lastPlayed` field in codes files to be a unix timestamp rather than a datetimestamp that requires a full parser. There's fallback code in the parser to handle the older format, though; subsequent writes will just write unix timestamps to the file instead. This entire bit should be transparent to users and is purely a convenience thing. This also makes a change to the public interface of the Rust lib: rather than passing over a fully qualified `user.json` path, we now just pass the folder where it's found and then build paths on the Rust side - since we need to do this for direct code file paths anyway. --- Cargo.lock | 15 ++ Cargo.toml | 2 +- exi/src/config.rs | 2 +- exi/src/lib.rs | 2 +- ffi/includes/SlippiRustExtensions.h | 30 ++- ffi/src/exi.rs | 4 +- ffi/src/user.rs | 88 +++++++++ user/Cargo.toml | 1 + user/src/direct_codes/last_played_parser.rs | 53 ++++++ user/src/direct_codes/mod.rs | 198 ++++++++++++++++++++ user/src/lib.rs | 33 +++- 11 files changed, 414 insertions(+), 14 deletions(-) create mode 100644 user/src/direct_codes/last_played_parser.rs create mode 100644 user/src/direct_codes/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 39fa9af..224fd04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,6 +337,9 @@ name = "deranged" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] [[package]] name = "dolphin-integrations" @@ -1182,6 +1185,7 @@ dependencies = [ "open", "serde", "serde_json", + "time", "tracing", "ureq", ] @@ -1291,10 +1295,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", + "itoa", "libc", "num_threads", "serde", "time-core", + "time-macros", ] [[package]] @@ -1303,6 +1309,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 9198c24..20249ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ debug = true panic = "abort" [workspace.dependencies] -time = { version = "0.3.20", default-features = false, features = ["std", "local-offset"] } +time = { version = "0.3.20", default-features = false, features = ["formatting", "parsing", "local-offset", "macros", "serde", "std"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } serde_repr = { version = "0.1" } diff --git a/exi/src/config.rs b/exi/src/config.rs index cf486a1..eb34fbf 100644 --- a/exi/src/config.rs +++ b/exi/src/config.rs @@ -2,7 +2,7 @@ #[derive(Debug)] pub struct FilePathsConfig { pub iso: String, - pub user_json: String, + pub user_config_folder: String, } /// Source control semver related parameters. diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 5f1885c..5331c1a 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -52,7 +52,7 @@ impl SlippiEXIDevice { let user_manager = UserManager::new( http_client.clone(), - config.paths.user_json.clone().into(), + config.paths.user_config_folder.clone().into(), config.scm.slippi_semver.clone(), ); diff --git a/ffi/includes/SlippiRustExtensions.h b/ffi/includes/SlippiRustExtensions.h index 6e50fb8..b1ecda3 100644 --- a/ffi/includes/SlippiRustExtensions.h +++ b/ffi/includes/SlippiRustExtensions.h @@ -4,6 +4,12 @@ #include #include +/// Indicates what type of direct code operation we're in. +enum DirectCodeKind { + Direct = 1, + Teams = 2, +}; + /// This enum is duplicated from `slippi_game_reporter::OnlinePlayMode` in order /// to appease cbindgen, which cannot see the type from the other module for /// inspection. @@ -25,7 +31,7 @@ enum SlippiMatchmakingOnlinePlayMode { /// structure. struct SlippiRustEXIConfig { const char *iso_path; - const char *user_json_path; + const char *user_config_folder; const char *scm_slippi_semver_str; void (*osd_add_msg_fn)(const char*, uint32_t, uint32_t); }; @@ -256,4 +262,26 @@ RustChatMessages *slprs_user_get_default_messages(uintptr_t exi_device_instance_ /// by converting it into the proper Rust types. void slprs_user_free_messages(RustChatMessages *ptr); +/// Passes along a direct code to add or update. +void slprs_user_direct_codes_add_or_update(uintptr_t exi_device_instance_ptr, + DirectCodeKind kind, + const char *code); + +/// Gets the length of the current direct codes stack for the given `kind`. +uint32_t slprs_user_direct_codes_get_length(uintptr_t exi_device_instance_ptr, DirectCodeKind kind); + +/// Checks to see if we have a direct code at `index`. +/// +/// This has the unfortunate aspect of going: Rust String -> CString -> C++ std::string, but +/// this will go away over time. Just be aware it's doing more allocations than is perhaps +/// ideal... but this area of code isn't performance sensitive anyway as it's not core +/// gameplay. +char *slprs_user_direct_codes_get_code_at_index(uintptr_t exi_device_instance_ptr, + DirectCodeKind kind, + uintptr_t index); + +/// As the allocator on the C++ could be different, we need to provide a `free` method +/// that the C++ side will call when it's handled everything it needs to do. +void slprs_user_direct_codes_free_code(char *code); + } // extern "C" diff --git a/ffi/src/exi.rs b/ffi/src/exi.rs index 1206964..e5d8f10 100644 --- a/ffi/src/exi.rs +++ b/ffi/src/exi.rs @@ -16,7 +16,7 @@ use crate::c_str_to_string; pub struct SlippiRustEXIConfig { // Paths pub iso_path: *const c_char, - pub user_json_path: *const c_char, + pub user_config_folder: *const c_char, // Git version number pub scm_slippi_semver_str: *const c_char, @@ -52,7 +52,7 @@ pub extern "C" fn slprs_exi_device_create(config: SlippiRustEXIConfig) -> usize let exi_device = Box::new(SlippiEXIDevice::new(Config { paths: FilePathsConfig { iso: c_str_to_string(config.iso_path, fn_name, "iso_path"), - user_json: c_str_to_string(config.user_json_path, fn_name, "user_json"), + user_config_folder: c_str_to_string(config.user_config_folder, fn_name, "user_config_folder"), }, scm: SCMConfig { diff --git a/ffi/src/user.rs b/ffi/src/user.rs index 7d44e90..d897f0d 100644 --- a/ffi/src/user.rs +++ b/ffi/src/user.rs @@ -235,3 +235,91 @@ pub extern "C" fn slprs_user_free_messages(ptr: *mut RustChatMessages) { } } } + +/// Indicates what type of direct code operation we're in. +#[repr(C)] +pub enum DirectCodeKind { + Direct = 1, + Teams = 2 +} + +/// Passes along a direct code to add or update. +#[no_mangle] +pub extern "C" fn slprs_user_direct_codes_add_or_update( + exi_device_instance_ptr: usize, + kind: DirectCodeKind, + code: *const c_char, +) { + let code = c_str_to_string(code, "slprs_user_add_or_update_direct_code", "code"); + + with::(exi_device_instance_ptr, move |device| { + match kind { + DirectCodeKind::Direct => { + device.user_manager.direct_codes.add_or_update_code(code); + }, + + DirectCodeKind::Teams => { + device.user_manager.teams_direct_codes.add_or_update_code(code); + } + } + }); +} + +/// Gets the length of the current direct codes stack for the given `kind`. +#[no_mangle] +pub extern "C" fn slprs_user_direct_codes_get_length( + exi_device_instance_ptr: usize, + kind: DirectCodeKind +) -> u32 { + with_returning::(exi_device_instance_ptr, move |device| { + match kind { + DirectCodeKind::Direct => { + device.user_manager.direct_codes.len() as u32 + }, + + DirectCodeKind::Teams => { + device.user_manager.teams_direct_codes.len() as u32 + } + } + }) +} + +/// Checks to see if we have a direct code at `index`. +/// +/// This has the unfortunate aspect of going: Rust String -> CString -> C++ std::string, but +/// this will go away over time. Just be aware it's doing more allocations than is perhaps +/// ideal... but this area of code isn't performance sensitive anyway as it's not core +/// gameplay. +#[no_mangle] +pub extern "C" fn slprs_user_direct_codes_get_code_at_index( + exi_device_instance_ptr: usize, + kind: DirectCodeKind, + index: usize +) -> *mut c_char { + let code = with_returning::(exi_device_instance_ptr, move |device| { + match kind { + DirectCodeKind::Direct => { + device.user_manager.direct_codes.get(index) + }, + + DirectCodeKind::Teams => { + device.user_manager.teams_direct_codes.get(index) + } + } + }); + + CString::new(code.as_bytes()) + .expect("Unable to convert direct code to CString") + .into_raw() +} + +/// As the allocator on the C++ could be different, we need to provide a `free` method +/// that the C++ side will call when it's handled everything it needs to do. +#[no_mangle] +pub extern "C" fn slprs_user_direct_codes_free_code(code: *mut c_char) { + unsafe { + if !code.is_null() { + let _ = CString::from_raw(code); + } + } +} diff --git a/user/Cargo.toml b/user/Cargo.toml index f75f0e0..dde029d 100644 --- a/user/Cargo.toml +++ b/user/Cargo.toml @@ -20,5 +20,6 @@ dolphin-integrations = { path = "../dolphin" } open = "5" serde = { workspace = true } serde_json = { workspace = true } +time = { workspace = true } tracing = { workspace = true } ureq = { workspace = true } diff --git a/user/src/direct_codes/last_played_parser.rs b/user/src/direct_codes/last_played_parser.rs new file mode 100644 index 0000000..54b0903 --- /dev/null +++ b/user/src/direct_codes/last_played_parser.rs @@ -0,0 +1,53 @@ +//! Implements deserialization/parsing the `last_played` field from direct +//! code file payloads. This will decode from either a unix timestamp *or* +//! an older used datetime string format. +//! +//! Subsequent writes to the direct codes file(s) will have their timstamps +//! written as i64 unix timestamps. This could potentially be done away with +//! after a few releases - just stub in the time crate macro for auto-generating +//! unix timestamp handling code. + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use time::macros::format_description; + +/// Serializes a timestamp as a unix timestamp (`i64`). +pub fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result +where + S: serde::Serializer, +{ + datetime.unix_timestamp().serialize(serializer) +} + +/// Attempts deserialiazation of the `last_played` field, by first checking if it's a +/// unix timestamp and falling back to the older timestamp format if not. +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + + if let Some(timestamp) = value.as_i64() { + return OffsetDateTime::from_unix_timestamp(timestamp) + .map_err(serde::de::Error::custom); + } + + if let Some(datetime_str) = value.as_str() { + let tsfmt = format_description!("[year][month][day]T[offset_hour][offset_minute][offset_second]"); + + return OffsetDateTime::parse(datetime_str, &tsfmt) + .map_err(serde::de::Error::custom); + } + + Err(serde::de::Error::custom(format!("Invalid last_played type in direct codes file: {:?}", value))) +} + +// Auto-generate serde parsers for the lastPlayed JSON field. +// Once we hit a point where we could just assume unix timestamps for all players, this module +// could go away and this macro could just be shoved into `mod.rs` - probably with a bit of +// tweaking but that's the gist of things. +/*time::serde::format_description!( + last_played_parser, + OffsetDateTime, + "[year][month][day]T[offset_hour][offset_minute][offset_second]" +);*/ diff --git a/user/src/direct_codes/mod.rs b/user/src/direct_codes/mod.rs new file mode 100644 index 0000000..6c7eed5 --- /dev/null +++ b/user/src/direct_codes/mod.rs @@ -0,0 +1,198 @@ +//! Direct codes are used on the connect screen as a form of history (codes +//! that have been recently connected to). + +use std::borrow::Cow; +use std::fs; +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use time::OffsetDateTime; + +use dolphin_integrations::Log; + +mod last_played_parser; + +/// Indicates how a sort of the direct codes should be done. +#[derive(Debug)] +enum SortBy { + // This sort type is not used at the moment, but was stubbed + // out in the C++ version. It's kept around commented out for + // marking potential future intentions. + // Name, + + LastPlayed +} + +/// The actual payload that's serialized back and forth to disk. +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct DirectCode { + #[serde(rename = "connectCode")] + pub connect_code: String, + + #[serde(rename = "lastPlayed", with = "last_played_parser")] + pub last_played: OffsetDateTime, + + // This doesn't exist yet and is stubbed to match the C++ version, + // which had some inkling of it - and could always be used in the + // future. + // #[serde(rename = "favorite")] + // pub is_favorite: Option +} + +/// A wrapper around a list of direct codes. The main entry point for querying, +/// sorting, and adding codes. This type is thread safe and be freely cloned and +/// passed around, though realistically only the user manager should need it. +#[derive(Clone, Debug)] +pub struct DirectCodes { + path: Arc, + codes: Arc>> +} + +impl DirectCodes { + /// Given a `path` that points to a user direct codes JSON file, will attempt + /// to load and deserialize the data. If either fails, this will log a message + /// indicating there's an issue but it will not error out - the underlying payload + /// will simply be empty. + pub fn load(path: PathBuf) -> Self { + tracing::info!(target: Log::SlippiOnline, ?path, "Attempting to load direct codes"); + + let mut codes = Vec::new(); + + match fs::read_to_string(path.as_path()) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(parsed) => { + codes = parsed; + }, + + Err(error) => { + tracing::error!(?error, "Unable to parse direct codes file"); + } + }, + + Err(error) => { + tracing::error!(?error, "Unable to read direct codes file"); + } + } + + Self { + path: Arc::new(path), + codes: Arc::new(Mutex::new(codes)) + } + } + + /// Sorts the underlying direct codes list by the `sort_by` parameter. + fn sort(codes: &mut Vec, sort_by: SortBy) { + match sort_by { + SortBy::LastPlayed => { + codes.sort_by(|a, b| a.last_played.cmp(&b.last_played)); + } + } + } + + /// Returns the length of the underlying direct codes list. + /// + /// This could generally be done with `Deref`, but needing a custom `sort` leads + /// me to think that this will be more clear in the long term how delegation is + /// happening. + pub fn len(&self) -> usize { + let codes = self.codes.lock() + .expect("Unable to lock codes for len check"); + + codes.len() + } + + /// Attempts to get the connect code at the specified index. + /// + /// This utilizes `Cow` (Copy-On-Write) to avoid extra allocations where + /// we don't perhaps need them. + pub fn get(&self, index: usize) -> Cow<'static, str> { + let mut codes = self.codes.lock() + .expect("Unable to lock codes for autocomplete"); + + Self::sort(&mut codes, SortBy::LastPlayed); + + if let Some(entry) = codes.get(index) { + return Cow::Owned(entry.connect_code.clone()); + } + + tracing::info!(target: Log::SlippiOnline, ?index, "Potential out of bounds name entry index"); + + Cow::Borrowed(match index >= codes.len() { + true => "1", + false => "" + }) + } + + /// Adds or updates a direct code. + /// + /// If it's an update, we're just updating the timestamp so that future sorts + /// order it appropriately. + pub fn add_or_update_code(&self, code: String) { + tracing::warn!(target: Log::SlippiOnline, ?code, "Attempting to add or update direct code"); + + let last_played = OffsetDateTime::now_utc(); + + let mut codes = self.codes.lock() + .expect("Unable to lock codes for autocomplete"); + + let mut found = false; + for mut entry in codes.iter_mut() { + if entry.connect_code == code { + found = true; + entry.last_played = last_played; + } + } + + if !found { + codes.push(DirectCode { + connect_code: code, + last_played + }); + } + + // Consider moving this to a background thread if the performance of + // `write_file` ever becomes an issue. In practice, it's never been one. + Self::write_file(self.path.as_path(), &codes); + } + + /* The below code is not used at the moment, but stubbed out to match the C++ side. + /// Attempts to autocomplete a code based off of the start text. + pub fn autocomplete(&self, start_text: &str) -> Option { + let mut codes = self.codes.lock() + .expect("Unable to lock codes for autocomplete"); + + Self::sort(&mut codes, SortBy::Time); + + for code in codes.iter() { + if code.connect_code.as_str().starts_with(start_text) { + return Some(code.connect_code.clone()); + } + } + + None + }*/ + + /// Serializes and writes the contents of `codes` to disk at `path`. + fn write_file(path: &Path, codes: &[DirectCode]) { + match fs::File::create(path) { + Ok(file) => { + let mut writer = BufWriter::new(file); + + if let Err(error) = serde_json::to_writer(&mut writer, codes) { + tracing::error!(target: Log::SlippiOnline, ?error, "Unable to write direct codes to disk"); + return; + } + + if let Err(error) = writer.flush() { + tracing::error!(target: Log::SlippiOnline, ?error, "Unable to flush direct codes file to disk"); + return; + } + }, + + Err(error) => { + tracing::error!(target: Log::SlippiOnline, ?error, ?path, "Unable to open direct codes file for write"); + } + } + } +} diff --git a/user/src/lib.rs b/user/src/lib.rs index dc37b04..e839b50 100644 --- a/user/src/lib.rs +++ b/user/src/lib.rs @@ -6,11 +6,12 @@ use std::sync::{Arc, Mutex}; use ureq::Agent; -// use dolphin_integrations::Log; - mod chat; pub use chat::DEFAULT_CHAT_MESSAGES; +mod direct_codes; +use direct_codes::DirectCodes; + mod watcher; use watcher::UserInfoWatcher; @@ -44,7 +45,6 @@ impl UserInfo { /// Mostly checks to make sure we're not loading or receiving anything undesired. pub fn sanitize(&mut self) { if self.chat_messages.is_none() || self.chat_messages.as_ref().unwrap().len() != 16 { - // if self.chat_messages.len() != 16 { self.chat_messages = Some(chat::default()); } } @@ -61,6 +61,8 @@ pub struct UserManager { http_client: Agent, user: Arc>, user_json_path: Arc, + pub direct_codes: DirectCodes, + pub teams_direct_codes: DirectCodes, slippi_semver: String, watcher: Arc>, } @@ -74,15 +76,33 @@ impl UserManager { /// this restriction via some assumptions. // @TODO: The semver param here should get refactored away in time once we've ironed out // how some things get persisted from the Dolphin side. Not a big deal to thread it for now. - pub fn new(http_client: Agent, user_json_path: PathBuf, slippi_semver: String) -> Self { + pub fn new(http_client: Agent, mut user_config_folder: PathBuf, slippi_semver: String) -> Self { + let direct_codes = DirectCodes::load({ + let mut path = user_config_folder.clone(); + path.push("direct-codes.json"); + path + }); + + let teams_direct_codes = DirectCodes::load({ + let mut path = user_config_folder.clone(); + path.push("teams-codes.json"); + path + }); + + let user_json_path = Arc::new({ + user_config_folder.push("user.json"); + user_config_folder + }); + let user = Arc::new(Mutex::new(UserInfo::default())); - let user_json_path = Arc::new(user_json_path); let watcher = Arc::new(Mutex::new(UserInfoWatcher::new())); Self { http_client, user, user_json_path, + direct_codes, + teams_direct_codes, slippi_semver, watcher, } @@ -96,9 +116,6 @@ impl UserManager { /// This is slightly better ergonomics wise than dealing with locking all over the place, and /// allows batch retrieval of properties. /// - /// If, in the rare event that a Mutex lock could not be acquired (which should... never - /// happen), this will call the provided closure with `&None` while logging the error. - /// /// ```no_run /// use slippi_user::UserManager; /// From e4888cea7a42b2fad7057766706210f51723a92a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 5 Feb 2024 16:09:31 -0800 Subject: [PATCH 2/4] Formatting pass --- ffi/src/user.rs | 57 ++++++++------------- user/src/direct_codes/last_played_parser.rs | 13 ++--- user/src/direct_codes/mod.rs | 44 ++++++++-------- 3 files changed, 49 insertions(+), 65 deletions(-) diff --git a/ffi/src/user.rs b/ffi/src/user.rs index d897f0d..8adce33 100644 --- a/ffi/src/user.rs +++ b/ffi/src/user.rs @@ -240,7 +240,7 @@ pub extern "C" fn slprs_user_free_messages(ptr: *mut RustChatMessages) { #[repr(C)] pub enum DirectCodeKind { Direct = 1, - Teams = 2 + Teams = 2, } /// Passes along a direct code to add or update. @@ -252,35 +252,24 @@ pub extern "C" fn slprs_user_direct_codes_add_or_update( ) { let code = c_str_to_string(code, "slprs_user_add_or_update_direct_code", "code"); - with::(exi_device_instance_ptr, move |device| { - match kind { - DirectCodeKind::Direct => { - device.user_manager.direct_codes.add_or_update_code(code); - }, - - DirectCodeKind::Teams => { - device.user_manager.teams_direct_codes.add_or_update_code(code); - } - } + with::(exi_device_instance_ptr, move |device| match kind { + DirectCodeKind::Direct => { + device.user_manager.direct_codes.add_or_update_code(code); + }, + + DirectCodeKind::Teams => { + device.user_manager.teams_direct_codes.add_or_update_code(code); + }, }); } /// Gets the length of the current direct codes stack for the given `kind`. #[no_mangle] -pub extern "C" fn slprs_user_direct_codes_get_length( - exi_device_instance_ptr: usize, - kind: DirectCodeKind -) -> u32 { - with_returning::(exi_device_instance_ptr, move |device| { - match kind { - DirectCodeKind::Direct => { - device.user_manager.direct_codes.len() as u32 - }, - - DirectCodeKind::Teams => { - device.user_manager.teams_direct_codes.len() as u32 - } - } +pub extern "C" fn slprs_user_direct_codes_get_length(exi_device_instance_ptr: usize, kind: DirectCodeKind) -> u32 { + with_returning::(exi_device_instance_ptr, move |device| match kind { + DirectCodeKind::Direct => device.user_manager.direct_codes.len() as u32, + + DirectCodeKind::Teams => device.user_manager.teams_direct_codes.len() as u32, }) } @@ -294,20 +283,14 @@ pub extern "C" fn slprs_user_direct_codes_get_length( pub extern "C" fn slprs_user_direct_codes_get_code_at_index( exi_device_instance_ptr: usize, kind: DirectCodeKind, - index: usize + index: usize, ) -> *mut c_char { - let code = with_returning::(exi_device_instance_ptr, move |device| { - match kind { - DirectCodeKind::Direct => { - device.user_manager.direct_codes.get(index) - }, - - DirectCodeKind::Teams => { - device.user_manager.teams_direct_codes.get(index) - } - } + let code = with_returning::(exi_device_instance_ptr, move |device| match kind { + DirectCodeKind::Direct => device.user_manager.direct_codes.get(index), + + DirectCodeKind::Teams => device.user_manager.teams_direct_codes.get(index), }); - + CString::new(code.as_bytes()) .expect("Unable to convert direct code to CString") .into_raw() diff --git a/user/src/direct_codes/last_played_parser.rs b/user/src/direct_codes/last_played_parser.rs index 54b0903..8c699d8 100644 --- a/user/src/direct_codes/last_played_parser.rs +++ b/user/src/direct_codes/last_played_parser.rs @@ -8,8 +8,8 @@ //! unix timestamp handling code. use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; use time::macros::format_description; +use time::OffsetDateTime; /// Serializes a timestamp as a unix timestamp (`i64`). pub fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result @@ -28,18 +28,19 @@ where let value = serde_json::Value::deserialize(deserializer)?; if let Some(timestamp) = value.as_i64() { - return OffsetDateTime::from_unix_timestamp(timestamp) - .map_err(serde::de::Error::custom); + return OffsetDateTime::from_unix_timestamp(timestamp).map_err(serde::de::Error::custom); } if let Some(datetime_str) = value.as_str() { let tsfmt = format_description!("[year][month][day]T[offset_hour][offset_minute][offset_second]"); - return OffsetDateTime::parse(datetime_str, &tsfmt) - .map_err(serde::de::Error::custom); + return OffsetDateTime::parse(datetime_str, &tsfmt).map_err(serde::de::Error::custom); } - Err(serde::de::Error::custom(format!("Invalid last_played type in direct codes file: {:?}", value))) + Err(serde::de::Error::custom(format!( + "Invalid last_played type in direct codes file: {:?}", + value + ))) } // Auto-generate serde parsers for the lastPlayed JSON field. diff --git a/user/src/direct_codes/mod.rs b/user/src/direct_codes/mod.rs index 6c7eed5..f3e3eb3 100644 --- a/user/src/direct_codes/mod.rs +++ b/user/src/direct_codes/mod.rs @@ -1,4 +1,4 @@ -//! Direct codes are used on the connect screen as a form of history (codes +//! Direct codes are used on the connect screen as a form of history (codes //! that have been recently connected to). use std::borrow::Cow; @@ -17,11 +17,10 @@ mod last_played_parser; #[derive(Debug)] enum SortBy { // This sort type is not used at the moment, but was stubbed - // out in the C++ version. It's kept around commented out for + // out in the C++ version. It's kept around commented out for // marking potential future intentions. // Name, - - LastPlayed + LastPlayed, } /// The actual payload that's serialized back and forth to disk. @@ -32,7 +31,6 @@ pub struct DirectCode { #[serde(rename = "lastPlayed", with = "last_played_parser")] pub last_played: OffsetDateTime, - // This doesn't exist yet and is stubbed to match the C++ version, // which had some inkling of it - and could always be used in the // future. @@ -46,7 +44,7 @@ pub struct DirectCode { #[derive(Clone, Debug)] pub struct DirectCodes { path: Arc, - codes: Arc>> + codes: Arc>>, } impl DirectCodes { @@ -67,17 +65,17 @@ impl DirectCodes { Err(error) => { tracing::error!(?error, "Unable to parse direct codes file"); - } + }, }, Err(error) => { tracing::error!(?error, "Unable to read direct codes file"); - } + }, } Self { path: Arc::new(path), - codes: Arc::new(Mutex::new(codes)) + codes: Arc::new(Mutex::new(codes)), } } @@ -86,7 +84,7 @@ impl DirectCodes { match sort_by { SortBy::LastPlayed => { codes.sort_by(|a, b| a.last_played.cmp(&b.last_played)); - } + }, } } @@ -96,8 +94,7 @@ impl DirectCodes { /// me to think that this will be more clear in the long term how delegation is /// happening. pub fn len(&self) -> usize { - let codes = self.codes.lock() - .expect("Unable to lock codes for len check"); + let codes = self.codes.lock().expect("Unable to lock codes for len check"); codes.len() } @@ -107,8 +104,7 @@ impl DirectCodes { /// This utilizes `Cow` (Copy-On-Write) to avoid extra allocations where /// we don't perhaps need them. pub fn get(&self, index: usize) -> Cow<'static, str> { - let mut codes = self.codes.lock() - .expect("Unable to lock codes for autocomplete"); + let mut codes = self.codes.lock().expect("Unable to lock codes for autocomplete"); Self::sort(&mut codes, SortBy::LastPlayed); @@ -120,7 +116,7 @@ impl DirectCodes { Cow::Borrowed(match index >= codes.len() { true => "1", - false => "" + false => "", }) } @@ -130,11 +126,10 @@ impl DirectCodes { /// order it appropriately. pub fn add_or_update_code(&self, code: String) { tracing::warn!(target: Log::SlippiOnline, ?code, "Attempting to add or update direct code"); - + let last_played = OffsetDateTime::now_utc(); - let mut codes = self.codes.lock() - .expect("Unable to lock codes for autocomplete"); + let mut codes = self.codes.lock().expect("Unable to lock codes for autocomplete"); let mut found = false; for mut entry in codes.iter_mut() { @@ -147,11 +142,11 @@ impl DirectCodes { if !found { codes.push(DirectCode { connect_code: code, - last_played + last_played, }); } - // Consider moving this to a background thread if the performance of + // Consider moving this to a background thread if the performance of // `write_file` ever becomes an issue. In practice, it's never been one. Self::write_file(self.path.as_path(), &codes); } @@ -191,8 +186,13 @@ impl DirectCodes { }, Err(error) => { - tracing::error!(target: Log::SlippiOnline, ?error, ?path, "Unable to open direct codes file for write"); - } + tracing::error!( + target: Log::SlippiOnline, + ?error, + ?path, + "Unable to open direct codes file for write" + ); + }, } } } From 5a704d47b9aaba7bc97d1e9da6be1eb81145e512 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 5 Feb 2024 22:08:27 -0800 Subject: [PATCH 3/4] Tweak cbindgen to guard itself a bit more, alter an FFI enum name so that c/cpp do not complain incessantly about overrides --- ffi/build.rs | 6 + ffi/includes/SlippiRustExtensions.h | 438 +++++++++++++++++----------- ffi/src/user.rs | 18 +- 3 files changed, 280 insertions(+), 182 deletions(-) diff --git a/ffi/build.rs b/ffi/build.rs index 6861699..0a5902f 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -14,8 +14,14 @@ fn main() { ..Default::default() }; + let warning = "/* Warning: this file is autogenerated by cbindgen. Don't modify this manually. */"; + let config = cbindgen::Config { + autogen_warning: Some(warning.into()), + cpp_compat: true, enumeration: enum_config, + language: cbindgen::Language::C, + pragma_once: true, ..Default::default() }; diff --git a/ffi/includes/SlippiRustExtensions.h b/ffi/includes/SlippiRustExtensions.h index b1ecda3..870fff0 100644 --- a/ffi/includes/SlippiRustExtensions.h +++ b/ffi/includes/SlippiRustExtensions.h @@ -1,129 +1,166 @@ -#include -#include -#include -#include -#include - -/// Indicates what type of direct code operation we're in. -enum DirectCodeKind { - Direct = 1, - Teams = 2, -}; - -/// This enum is duplicated from `slippi_game_reporter::OnlinePlayMode` in order -/// to appease cbindgen, which cannot see the type from the other module for -/// inspection. -/// -/// This enum will likely go away as things move towards Rust, since it's effectively -/// just C FFI glue code. -enum SlippiMatchmakingOnlinePlayMode { +#pragma once + +/* Warning: this file is autogenerated by cbindgen. Don't modify this manually. */ + +#include +#include +#include +#include + +/** + * Indicates what type of direct code operation we're in. + */ +typedef enum DirectCodeKind { + DirectCodes = 1, + TeamsCodes = 2, +} DirectCodeKind; + +/** + * This enum is duplicated from `slippi_game_reporter::OnlinePlayMode` in order + * to appease cbindgen, which cannot see the type from the other module for + * inspection. + * + * This enum will likely go away as things move towards Rust, since it's effectively + * just C FFI glue code. + */ +typedef enum SlippiMatchmakingOnlinePlayMode { Ranked = 0, Unranked = 1, Direct = 2, Teams = 3, -}; - -/// A configuration struct for passing over certain argument types from the C/C++ side. -/// -/// The number of arguments necessary to shuttle across the FFI boundary when starting the -/// EXI device is higher than ideal at the moment, though it should lessen with time. For now, -/// this struct exists to act as a slightly more sane approach to readability of the args -/// structure. -struct SlippiRustEXIConfig { +} SlippiMatchmakingOnlinePlayMode; + +/** + * A configuration struct for passing over certain argument types from the C/C++ side. + * + * The number of arguments necessary to shuttle across the FFI boundary when starting the + * EXI device is higher than ideal at the moment, though it should lessen with time. For now, + * this struct exists to act as a slightly more sane approach to readability of the args + * structure. + */ +typedef struct SlippiRustEXIConfig { const char *iso_path; const char *user_config_folder; const char *scm_slippi_semver_str; void (*osd_add_msg_fn)(const char*, uint32_t, uint32_t); -}; - -/// An intermediary type for moving `UserInfo` across the FFI boundary. -/// -/// This type is C compatible, and we coerce Rust types into C types for this struct to -/// ease passing things over. This must be free'd on the Rust side via `slprs_user_free_info`. -struct RustUserInfo { +} SlippiRustEXIConfig; + +/** + * An intermediary type for moving `UserInfo` across the FFI boundary. + * + * This type is C compatible, and we coerce Rust types into C types for this struct to + * ease passing things over. This must be free'd on the Rust side via `slprs_user_free_info`. + */ +typedef struct RustUserInfo { const char *uid; const char *play_key; const char *display_name; const char *connect_code; const char *latest_version; -}; - -/// An intermediary type for moving chat messages across the FFI boundary. -/// -/// This type is C compatible, and we coerce Rust types into C types for this struct to -/// ease passing things over. This must be free'd on the Rust side via `slprs_user_free_messages`. -struct RustChatMessages { +} RustUserInfo; + +/** + * An intermediary type for moving chat messages across the FFI boundary. + * + * This type is C compatible, and we coerce Rust types into C types for this struct to + * ease passing things over. This must be free'd on the Rust side via `slprs_user_free_messages`. + */ +typedef struct RustChatMessages { char **data; int len; -}; +} RustChatMessages; +#ifdef __cplusplus extern "C" { - -/// Creates and leaks a shadow EXI device with the provided configuration. -/// -/// The C++ (Dolphin) side of things should call this and pass the appropriate arguments. At -/// that point, everything on the Rust side is its own universe, and should be told to shut -/// down (at whatever point) via the corresponding `slprs_exi_device_destroy` function. -/// -/// The returned pointer from this should *not* be used after calling `slprs_exi_device_destroy`. -uintptr_t slprs_exi_device_create(SlippiRustEXIConfig config); - -/// The C++ (Dolphin) side of things should call this to notify the Rust side that it -/// can safely shut down and clean up. +#endif // __cplusplus + +/** + * Creates and leaks a shadow EXI device with the provided configuration. + * + * The C++ (Dolphin) side of things should call this and pass the appropriate arguments. At + * that point, everything on the Rust side is its own universe, and should be told to shut + * down (at whatever point) via the corresponding `slprs_exi_device_destroy` function. + * + * The returned pointer from this should *not* be used after calling `slprs_exi_device_destroy`. + */ +uintptr_t slprs_exi_device_create(struct SlippiRustEXIConfig config); + +/** + * The C++ (Dolphin) side of things should call this to notify the Rust side that it + * can safely shut down and clean up. + */ void slprs_exi_device_destroy(uintptr_t exi_device_instance_ptr); -/// This method should be called from the EXI device subclass shim that's registered on -/// the Dolphin side, corresponding to: -/// -/// `virtual void DMAWrite(u32 _uAddr, u32 _uSize);` +/** + * This method should be called from the EXI device subclass shim that's registered on + * the Dolphin side, corresponding to: + * + * `virtual void DMAWrite(u32 _uAddr, u32 _uSize);` + */ void slprs_exi_device_dma_write(uintptr_t exi_device_instance_ptr, const uint8_t *address, const uint8_t *size); -/// This method should be called from the EXI device subclass shim that's registered on -/// the Dolphin side, corresponding to: -/// -/// `virtual void DMARead(u32 _uAddr, u32 _uSize);` +/** + * This method should be called from the EXI device subclass shim that's registered on + * the Dolphin side, corresponding to: + * + * `virtual void DMARead(u32 _uAddr, u32 _uSize);` + */ void slprs_exi_device_dma_read(uintptr_t exi_device_instance_ptr, const uint8_t *address, const uint8_t *size); -/// Moves ownership of the `GameReport` at the specified address to the -/// `SlippiGameReporter` on the EXI Device the corresponding address. This -/// will then add it to the processing pipeline. -/// -/// The reporter will manage the actual... reporting. +/** + * Moves ownership of the `GameReport` at the specified address to the + * `SlippiGameReporter` on the EXI Device the corresponding address. This + * will then add it to the processing pipeline. + * + * The reporter will manage the actual... reporting. + */ void slprs_exi_device_log_game_report(uintptr_t instance_ptr, uintptr_t game_report_instance_ptr); -/// Calls through to `SlippiGameReporter::start_new_session`. +/** + * Calls through to `SlippiGameReporter::start_new_session`. + */ void slprs_exi_device_start_new_reporter_session(uintptr_t instance_ptr); -/// Calls through to the `SlippiGameReporter` on the EXI device to report a -/// match completion event. +/** + * Calls through to the `SlippiGameReporter` on the EXI device to report a + * match completion event. + */ void slprs_exi_device_report_match_completion(uintptr_t instance_ptr, const char *match_id, uint8_t end_mode); -/// Calls through to the `SlippiGameReporter` on the EXI device to report a -/// match abandon event. +/** + * Calls through to the `SlippiGameReporter` on the EXI device to report a + * match abandon event. + */ void slprs_exi_device_report_match_abandonment(uintptr_t instance_ptr, const char *match_id); -/// Calls through to `SlippiGameReporter::push_replay_data`. +/** + * Calls through to `SlippiGameReporter::push_replay_data`. + */ void slprs_exi_device_reporter_push_replay_data(uintptr_t instance_ptr, const uint8_t *data, uint32_t length); -/// Configures the Jukebox process. This needs to be called after the EXI device is created -/// in order for certain pieces of Dolphin to be properly initalized; this may change down -/// the road though and is not set in stone. +/** + * Configures the Jukebox process. This needs to be called after the EXI device is created + * in order for certain pieces of Dolphin to be properly initalized; this may change down + * the road though and is not set in stone. + */ void slprs_exi_device_configure_jukebox(uintptr_t exi_device_instance_ptr, bool is_enabled, uint8_t initial_dolphin_system_volume, uint8_t initial_dolphin_music_volume); -/// Creates a new Player Report and leaks it, returning the pointer. -/// -/// This should be passed on to a GameReport for processing. +/** + * Creates a new Player Report and leaks it, returning the pointer. + * + * This should be passed on to a GameReport for processing. + */ uintptr_t slprs_player_report_create(const char *uid, uint8_t slot_type, double damage_done, @@ -133,14 +170,16 @@ uintptr_t slprs_player_report_create(const char *uid, int64_t starting_stocks, int64_t starting_percent); -/// Creates a new GameReport and leaks it, returning the instance pointer -/// after doing so. -/// -/// This is expected to ultimately be passed to the game reporter, which will handle -/// destruction and cleanup. +/** + * Creates a new GameReport and leaks it, returning the instance pointer + * after doing so. + * + * This is expected to ultimately be passed to the game reporter, which will handle + * destruction and cleanup. + */ uintptr_t slprs_game_report_create(const char *uid, const char *play_key, - SlippiMatchmakingOnlinePlayMode online_mode, + enum SlippiMatchmakingOnlinePlayMode online_mode, const char *match_id, uint32_t duration_frames, uint32_t game_index, @@ -150,138 +189,193 @@ uintptr_t slprs_game_report_create(const char *uid, int8_t lras_initiator, int32_t stage_id); -/// Takes ownership of the `PlayerReport` at the specified pointer, adding it to the -/// `GameReport` at the corresponding pointer. +/** + * Takes ownership of the `PlayerReport` at the specified pointer, adding it to the + * `GameReport` at the corresponding pointer. + */ void slprs_game_report_add_player_report(uintptr_t instance_ptr, uintptr_t player_report_instance_ptr); -/// Calls through to `Jukebox::start_song`. +/** + * Calls through to `Jukebox::start_song`. + */ void slprs_jukebox_start_song(uintptr_t exi_device_instance_ptr, uint64_t hps_offset, uintptr_t hps_length); -/// Calls through to `Jukebox::stop_music`. +/** + * Calls through to `Jukebox::stop_music`. + */ void slprs_jukebox_stop_music(uintptr_t exi_device_instance_ptr); -/// Calls through to `Jukebox::set_volume` with the Melee volume control. +/** + * Calls through to `Jukebox::set_volume` with the Melee volume control. + */ void slprs_jukebox_set_melee_music_volume(uintptr_t exi_device_instance_ptr, uint8_t volume); -/// Calls through to `Jukebox::set_volume` with the DolphinSystem volume control. +/** + * Calls through to `Jukebox::set_volume` with the DolphinSystem volume control. + */ void slprs_jukebox_set_dolphin_system_volume(uintptr_t exi_device_instance_ptr, uint8_t volume); -/// Calls through to `Jukebox::set_volume` with the DolphinMusic volume control. +/** + * Calls through to `Jukebox::set_volume` with the DolphinMusic volume control. + */ void slprs_jukebox_set_dolphin_music_volume(uintptr_t exi_device_instance_ptr, uint8_t volume); -/// This should be called from the Dolphin LogManager initialization to ensure that -/// all logging needs on the Rust side are configured appropriately. -/// -/// For more information, consult `dolphin_logger::init`. -/// -/// Note that `logger_fn` cannot be type-aliased here, otherwise cbindgen will -/// mess up the header output. That said, the function type represents: -/// -/// ``` -/// void Log(level, log_type, msg); -/// ``` +/** + * This should be called from the Dolphin LogManager initialization to ensure that + * all logging needs on the Rust side are configured appropriately. + * + * For more information, consult `dolphin_logger::init`. + * + * Note that `logger_fn` cannot be type-aliased here, otherwise cbindgen will + * mess up the header output. That said, the function type represents: + * + * ``` + * void Log(level, log_type, msg); + * ``` + */ void slprs_logging_init(void (*logger_fn)(int, int, const char*)); -/// Registers a log container, which mirrors a Dolphin `LogContainer` (`RustLogContainer`). -/// -/// See `dolphin_logger::register_container` for more information. +/** + * Registers a log container, which mirrors a Dolphin `LogContainer` (`RustLogContainer`). + * + * See `dolphin_logger::register_container` for more information. + */ void slprs_logging_register_container(const char *kind, int log_type, bool is_enabled, int default_log_level); -/// Updates the configuration for a registered logging container. -/// -/// For more information, see `dolphin_logger::update_container`. +/** + * Updates the configuration for a registered logging container. + * + * For more information, see `dolphin_logger::update_container`. + */ void slprs_logging_update_container(const char *kind, bool enabled, int level); -/// Updates the configuration for registered logging container on mainline -/// -/// For more information, see `dolphin_logger::update_container`. +/** + * Updates the configuration for registered logging container on mainline + * + * For more information, see `dolphin_logger::update_container`. + */ void slprs_mainline_logging_update_log_level(int level); -/// Instructs the `UserManager` on the EXI Device at the provided pointer to attempt -/// authentication. This runs synchronously on whatever thread it's called on. +/** + * Instructs the `UserManager` on the EXI Device at the provided pointer to attempt + * authentication. This runs synchronously on whatever thread it's called on. + */ bool slprs_user_attempt_login(uintptr_t exi_device_instance_ptr); -/// Instructs the `UserManager` on the EXI Device at the provided pointer to try to -/// open the login page in a system-provided browser view. +/** + * Instructs the `UserManager` on the EXI Device at the provided pointer to try to + * open the login page in a system-provided browser view. + */ void slprs_user_open_login_page(uintptr_t exi_device_instance_ptr); -/// Instructs the `UserManager` on the EXI Device at the provided pointer to attempt -/// to initiate the older update flow. +/** + * Instructs the `UserManager` on the EXI Device at the provided pointer to attempt + * to initiate the older update flow. + */ bool slprs_user_update_app(uintptr_t exi_device_instance_ptr); -/// Instructs the `UserManager` on the EXI Device at the provided pointer to start watching -/// for the presence of a `user.json` file. The `UserManager` should have the requisite path -/// already from EXI device instantiation. +/** + * Instructs the `UserManager` on the EXI Device at the provided pointer to start watching + * for the presence of a `user.json` file. The `UserManager` should have the requisite path + * already from EXI device instantiation. + */ void slprs_user_listen_for_login(uintptr_t exi_device_instance_ptr); -/// Instructs the `UserManager` on the EXI Device at the provided pointer to sign the user out. -/// This will delete the `user.json` file from the underlying filesystem. +/** + * Instructs the `UserManager` on the EXI Device at the provided pointer to sign the user out. + * This will delete the `user.json` file from the underlying filesystem. + */ void slprs_user_logout(uintptr_t exi_device_instance_ptr); -/// Hooks through the `UserManager` on the EXI Device at the provided pointer to overwrite the -/// latest version field on the current user. +/** + * Hooks through the `UserManager` on the EXI Device at the provided pointer to overwrite the + * latest version field on the current user. + */ void slprs_user_overwrite_latest_version(uintptr_t exi_device_instance_ptr, const char *version); -/// Hooks through the `UserManager` on the EXI Device at the provided pointer to determine -/// authentication status. +/** + * Hooks through the `UserManager` on the EXI Device at the provided pointer to determine + * authentication status. + */ bool slprs_user_get_is_logged_in(uintptr_t exi_device_instance_ptr); -/// Hooks through the `UserManager` on the EXI Device at the provided pointer to get information -/// for the current user. This then wraps it in a C struct to pass back so that ownership is safely -/// moved. -/// -/// This involves slightly more allocations than ideal, so this shouldn't be called in a hot path. -/// Over time this issue will not matter as once Matchmaking is moved to Rust we can share things -/// quite easily. -RustUserInfo *slprs_user_get_info(uintptr_t exi_device_instance_ptr); - -/// Takes ownership back of a `UserInfo` struct and drops it. -/// -/// When the C/C++ side grabs `UserInfo`, it needs to ensure that it's passed back to Rust -/// to ensure that the memory layout matches - do _not_ call `free` on `UserInfo`, pass it here -/// instead. -void slprs_user_free_info(RustUserInfo *ptr); - -/// Returns a C-compatible struct containing the chat message options for the current user. -/// -/// The return value of this _must_ be passed back to `slprs_user_free_messages` to free memory. -RustChatMessages *slprs_user_get_messages(uintptr_t exi_device_instance_ptr); - -/// Returns a C-compatible struct containing the default chat message options. -/// -/// The return value of this _must_ be passed back to `slprs_user_free_messages` to free memory. -RustChatMessages *slprs_user_get_default_messages(uintptr_t exi_device_instance_ptr); - -/// Takes back ownership of a `RustChatMessages` instance and frees the underlying data -/// by converting it into the proper Rust types. -void slprs_user_free_messages(RustChatMessages *ptr); - -/// Passes along a direct code to add or update. +/** + * Hooks through the `UserManager` on the EXI Device at the provided pointer to get information + * for the current user. This then wraps it in a C struct to pass back so that ownership is safely + * moved. + * + * This involves slightly more allocations than ideal, so this shouldn't be called in a hot path. + * Over time this issue will not matter as once Matchmaking is moved to Rust we can share things + * quite easily. + */ +struct RustUserInfo *slprs_user_get_info(uintptr_t exi_device_instance_ptr); + +/** + * Takes ownership back of a `UserInfo` struct and drops it. + * + * When the C/C++ side grabs `UserInfo`, it needs to ensure that it's passed back to Rust + * to ensure that the memory layout matches - do _not_ call `free` on `UserInfo`, pass it here + * instead. + */ +void slprs_user_free_info(struct RustUserInfo *ptr); + +/** + * Returns a C-compatible struct containing the chat message options for the current user. + * + * The return value of this _must_ be passed back to `slprs_user_free_messages` to free memory. + */ +struct RustChatMessages *slprs_user_get_messages(uintptr_t exi_device_instance_ptr); + +/** + * Returns a C-compatible struct containing the default chat message options. + * + * The return value of this _must_ be passed back to `slprs_user_free_messages` to free memory. + */ +struct RustChatMessages *slprs_user_get_default_messages(uintptr_t exi_device_instance_ptr); + +/** + * Takes back ownership of a `RustChatMessages` instance and frees the underlying data + * by converting it into the proper Rust types. + */ +void slprs_user_free_messages(struct RustChatMessages *ptr); + +/** + * Passes along a direct code to add or update. + */ void slprs_user_direct_codes_add_or_update(uintptr_t exi_device_instance_ptr, - DirectCodeKind kind, + enum DirectCodeKind kind, const char *code); -/// Gets the length of the current direct codes stack for the given `kind`. -uint32_t slprs_user_direct_codes_get_length(uintptr_t exi_device_instance_ptr, DirectCodeKind kind); - -/// Checks to see if we have a direct code at `index`. -/// -/// This has the unfortunate aspect of going: Rust String -> CString -> C++ std::string, but -/// this will go away over time. Just be aware it's doing more allocations than is perhaps -/// ideal... but this area of code isn't performance sensitive anyway as it's not core -/// gameplay. +/** + * Gets the length of the current direct codes stack for the given `kind`. + */ +uint32_t slprs_user_direct_codes_get_length(uintptr_t exi_device_instance_ptr, + enum DirectCodeKind kind); + +/** + * Checks to see if we have a direct code at `index`. + * + * This has the unfortunate aspect of going: Rust String -> CString -> C++ std::string, but + * this will go away over time. Just be aware it's doing more allocations than is perhaps + * ideal... but this area of code isn't performance sensitive anyway as it's not core + * gameplay. + */ char *slprs_user_direct_codes_get_code_at_index(uintptr_t exi_device_instance_ptr, - DirectCodeKind kind, + enum DirectCodeKind kind, uintptr_t index); -/// As the allocator on the C++ could be different, we need to provide a `free` method -/// that the C++ side will call when it's handled everything it needs to do. +/** + * As the allocator on the C++ could be different, we need to provide a `free` method + * that the C++ side will call when it's handled everything it needs to do. + */ void slprs_user_direct_codes_free_code(char *code); +#ifdef __cplusplus } // extern "C" +#endif // __cplusplus diff --git a/ffi/src/user.rs b/ffi/src/user.rs index 8adce33..1179f9d 100644 --- a/ffi/src/user.rs +++ b/ffi/src/user.rs @@ -239,8 +239,8 @@ pub extern "C" fn slprs_user_free_messages(ptr: *mut RustChatMessages) { /// Indicates what type of direct code operation we're in. #[repr(C)] pub enum DirectCodeKind { - Direct = 1, - Teams = 2, + DirectCodes = 1, + TeamsCodes = 2, } /// Passes along a direct code to add or update. @@ -253,11 +253,11 @@ pub extern "C" fn slprs_user_direct_codes_add_or_update( let code = c_str_to_string(code, "slprs_user_add_or_update_direct_code", "code"); with::(exi_device_instance_ptr, move |device| match kind { - DirectCodeKind::Direct => { + DirectCodeKind::DirectCodes => { device.user_manager.direct_codes.add_or_update_code(code); }, - DirectCodeKind::Teams => { + DirectCodeKind::TeamsCodes => { device.user_manager.teams_direct_codes.add_or_update_code(code); }, }); @@ -267,9 +267,8 @@ pub extern "C" fn slprs_user_direct_codes_add_or_update( #[no_mangle] pub extern "C" fn slprs_user_direct_codes_get_length(exi_device_instance_ptr: usize, kind: DirectCodeKind) -> u32 { with_returning::(exi_device_instance_ptr, move |device| match kind { - DirectCodeKind::Direct => device.user_manager.direct_codes.len() as u32, - - DirectCodeKind::Teams => device.user_manager.teams_direct_codes.len() as u32, + DirectCodeKind::DirectCodes => device.user_manager.direct_codes.len() as u32, + DirectCodeKind::TeamsCodes => device.user_manager.teams_direct_codes.len() as u32, }) } @@ -286,9 +285,8 @@ pub extern "C" fn slprs_user_direct_codes_get_code_at_index( index: usize, ) -> *mut c_char { let code = with_returning::(exi_device_instance_ptr, move |device| match kind { - DirectCodeKind::Direct => device.user_manager.direct_codes.get(index), - - DirectCodeKind::Teams => device.user_manager.teams_direct_codes.get(index), + DirectCodeKind::DirectCodes => device.user_manager.direct_codes.get(index), + DirectCodeKind::TeamsCodes => device.user_manager.teams_direct_codes.get(index), }); CString::new(code.as_bytes()) From d9e3221abb7e1fa1d12bbc1d0220d826cda9951e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 5 Feb 2024 23:05:03 -0800 Subject: [PATCH 4/4] Add an alias for mainline keys. It looks like the mainline build might use different cased keys, so adding an alias here so they all centralize and the lib can work across builds. --- user/src/direct_codes/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/src/direct_codes/mod.rs b/user/src/direct_codes/mod.rs index f3e3eb3..b2c3d50 100644 --- a/user/src/direct_codes/mod.rs +++ b/user/src/direct_codes/mod.rs @@ -26,10 +26,10 @@ enum SortBy { /// The actual payload that's serialized back and forth to disk. #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct DirectCode { - #[serde(rename = "connectCode")] + #[serde(rename = "connectCode", alias = "connect_code")] pub connect_code: String, - #[serde(rename = "lastPlayed", with = "last_played_parser")] + #[serde(rename = "lastPlayed", alias = "last_played", with = "last_played_parser")] pub last_played: OffsetDateTime, // This doesn't exist yet and is stubbed to match the C++ version, // which had some inkling of it - and could always be used in the