From af6087842947eb36e1358484aa1fc88a56132b32 Mon Sep 17 00:00:00 2001 From: Maple Nebel Date: Mon, 8 Jun 2026 10:31:55 +0200 Subject: [PATCH] 3ds friends --- rnex-core/build.rs | 13 + .../src/rmc/protocols/account_management.rs | 2 +- rnex-core/src/rmc/protocols/friends_3ds.rs | 250 ++++++++++++++++++ .../protocols/{friends.rs => friends_wiiu.rs} | 2 +- rnex-core/src/rmc/protocols/mod.rs | 3 +- rnex-core/src/server_api/gatherings.rs | 88 ++++++ rnex-core/src/server_api/meta.rs | 36 +++ rnex-core/src/server_api/mod.rs | 22 ++ rnex-server-api-grpc/gatherings.proto | 54 ++++ rnex-server-api-grpc/meta.proto | 27 ++ rnex-server-api/Cargo.toml | 12 + rnex-server-api/build.rs | 24 ++ rnex-server-api/src/lib.rs | 6 + 13 files changed, 536 insertions(+), 3 deletions(-) create mode 100644 rnex-core/build.rs create mode 100644 rnex-core/src/rmc/protocols/friends_3ds.rs rename rnex-core/src/rmc/protocols/{friends.rs => friends_wiiu.rs} (99%) create mode 100644 rnex-core/src/server_api/gatherings.rs create mode 100644 rnex-core/src/server_api/meta.rs create mode 100644 rnex-core/src/server_api/mod.rs create mode 100644 rnex-server-api-grpc/gatherings.proto create mode 100644 rnex-server-api-grpc/meta.proto create mode 100644 rnex-server-api/Cargo.toml create mode 100644 rnex-server-api/build.rs create mode 100644 rnex-server-api/src/lib.rs diff --git a/rnex-core/build.rs b/rnex-core/build.rs new file mode 100644 index 0000000..2af0e5f --- /dev/null +++ b/rnex-core/build.rs @@ -0,0 +1,13 @@ +use std::{env, process::Command}; +fn main() { + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + println!( + "cargo:rustc-env=FEATURESET={}", + env::var("CARGO_CFG_FEATURE").unwrap() + ); +} diff --git a/rnex-core/src/rmc/protocols/account_management.rs b/rnex-core/src/rmc/protocols/account_management.rs index 8ef2d9e..a245fe7 100644 --- a/rnex-core/src/rmc/protocols/account_management.rs +++ b/rnex-core/src/rmc/protocols/account_management.rs @@ -5,7 +5,7 @@ use rnex_core::{ rmc::{response::ErrorCode, structures::any::Any}, }; -use crate::{kerberos::KerberosDateTime, rmc::protocols::friends::NNAInfo}; +use crate::{kerberos::KerberosDateTime, rmc::protocols::friends_wiiu::NNAInfo}; #[derive(RmcSerialize, Debug, Clone)] #[rmc_struct(0)] diff --git a/rnex-core/src/rmc/protocols/friends_3ds.rs b/rnex-core/src/rmc/protocols/friends_3ds.rs new file mode 100644 index 0000000..aaa7af0 --- /dev/null +++ b/rnex-core/src/rmc/protocols/friends_3ds.rs @@ -0,0 +1,250 @@ +use macros::{RmcSerialize, method_id, rmc_proto}; + +use rnex_core::rmc::{response::ErrorCode, structures::data::Data}; + +use crate::{PID, kerberos::KerberosDateTime}; + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct MyProfile { + #[extends] + pub data: Data, + pub region: u8, + pub country: u8, + pub area: u8, + pub language: u8, + pub platform: u8, + pub local_friend_code_seed: u64, + pub mac_address: String, + pub serial_number: String, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct Mii { + #[extends] + pub data: Data, + pub unk1: String, + pub unk2: bool, + pub unk3: u8, + pub mii_data: Vec, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct MiiList { + #[extends] + pub data: Data, + pub unk1: String, + pub unk2: bool, + pub unk3: u8, + pub mii_data: Vec>, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct GameKey { + #[extends] + pub data: Data, + pub title_id: u64, + pub version: u16, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct PlayedGame { + #[extends] + pub data: Data, + pub game_key: GameKey, + pub date_time: KerberosDateTime, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendInfo { + pub unk1: u32, + pub unk2: KerberosDateTime, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendMii { + #[extends] + pub data: Data, + pub pid: PID, + pub mii: Mii, + pub modified_at: KerberosDateTime, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendMiiList { + #[extends] + pub data: Data, + pub unk1: u32, + pub mii_list: MiiList, + pub unk2: KerberosDateTime, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendRelationship { + #[extends] + pub data: Data, + pub unk1: u32, + pub unk2: u64, + pub unk3: u8, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct NintendoPresence { + #[extends] + pub data: Data, + pub changed_bit_flag: u32, + pub game_key: GameKey, + pub game_mode_desctiption: String, + pub join_availibility_flag: u32, + pub mm_system_type: u8, + pub join_game_id: u32, + pub join_game_mode: u32, + pub owner_pid: PID, + pub join_group_id: u32, + pub application_arg: Vec, +} +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendPresence { + #[extends] + pub data: Data, + pub unk: u32, + pub presence: NintendoPresence, +} +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendComment { + #[extends] + pub data: Data, + pub pid: PID, + pub comment: String, + pub modified_at: KerberosDateTime, +} +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendPicture { + #[extends] + pub data: Data, + pub unk1: u32, + pub pic_data: Vec, + pub date_time: KerberosDateTime, +} + +#[derive(RmcSerialize)] +#[rmc_struct(0)] +pub struct FriendPersistentInfo { + #[extends] + pub data: Data, + pub pid: PID, + pub region: u8, + pub country: u8, + pub area: u8, + pub language: u8, + pub platform: u8, + pub game_key: GameKey, + pub message: String, + pub msg_updated_at: KerberosDateTime, + pub friended_at: KerberosDateTime, + pub last_online: KerberosDateTime, +} + +#[rmc_proto(101)] +pub trait Friends3DS { + #[method_id(1)] + async fn update_profile(&self, profile: MyProfile) -> Result<(), ErrorCode>; + #[method_id(2)] + async fn update_mii(&self, profile: Mii) -> Result<(), ErrorCode>; + #[method_id(3)] + async fn update_mii_list(&self, profile: MiiList) -> Result<(), ErrorCode>; + #[method_id(4)] + async fn update_played_games(&self, profile: Vec) -> Result<(), ErrorCode>; + #[method_id(5)] + async fn update_preference( + &self, + show_online_status: bool, + show_current_title: bool, + block_friend_requests: bool, + ) -> Result<(), ErrorCode>; + + #[method_id(6)] + async fn get_friend_mii(&self, friends: Vec) -> Result, ErrorCode>; + #[method_id(7)] + async fn get_friend_mii_list( + &self, + friends: Vec, + ) -> Result, ErrorCode>; + #[method_id(8)] + async fn is_active_game(&self, unk: Vec, game_key: GameKey) + -> Result, ErrorCode>; + #[method_id(9)] + async fn get_principal_id_by_local_friend_code( + &self, + unk1: u64, + unk2: Vec, + ) -> Result, ErrorCode>; + #[method_id(10)] + async fn get_friend_relationships( + &self, + unk2: Vec, + ) -> Result, ErrorCode>; + #[method_id(11)] + async fn add_friend_by_pid(&self, unk: u64, pid: PID) -> Result; + #[method_id(12)] + async fn add_friend_by_lst_pid( + &self, + unk: u64, + pid: Vec, + ) -> Result, ErrorCode>; + #[method_id(13)] + async fn remove_friend_by_local_code(&self, local_code: u64) -> Result<(), ErrorCode>; + #[method_id(14)] + async fn remove_friend_by_pid(&self, pid: PID) -> Result<(), ErrorCode>; + #[method_id(15)] + async fn get_all_friends(&self) -> Result, ErrorCode>; + #[method_id(16)] + async fn update_blacklist(&self) -> Result<(), ErrorCode>; + #[method_id(17)] + async fn sync_friend( + &self, + unk1: u64, + unk2: Vec, + unk3: Vec, + ) -> Result, ErrorCode>; + #[method_id(18)] + async fn update_presence( + &self, + nintendo_presence: NintendoPresence, + unk: bool, + ) -> Result<(), ErrorCode>; + #[method_id(19)] + async fn update_favorite_game_key(&self, game_key: GameKey) -> Result<(), ErrorCode>; + #[method_id(20)] + async fn update_comment(&self, comment: String) -> Result<(), ErrorCode>; + #[method_id(21)] + async fn update_picture(&self, unk: u32, picture: Vec) -> Result<(), ErrorCode>; + #[method_id(22)] + async fn get_friend_presence(&self, unk: Vec) -> Result, ErrorCode>; + #[method_id(23)] + async fn get_friend_comment( + &self, + unk: Vec, + ) -> Result, ErrorCode>; + #[method_id(24)] + async fn get_friend_picture(&self, unk: Vec) -> Result, ErrorCode>; + #[method_id(25)] + async fn get_friend_persistent_info( + &self, + unk: Vec, + ) -> Result, ErrorCode>; + #[method_id(26)] + async fn send_invitation(&self, unk: Vec) -> Result<(), ErrorCode>; +} diff --git a/rnex-core/src/rmc/protocols/friends.rs b/rnex-core/src/rmc/protocols/friends_wiiu.rs similarity index 99% rename from rnex-core/src/rmc/protocols/friends.rs rename to rnex-core/src/rmc/protocols/friends_wiiu.rs index d25ea9e..a539008 100644 --- a/rnex-core/src/rmc/protocols/friends.rs +++ b/rnex-core/src/rmc/protocols/friends_wiiu.rs @@ -148,7 +148,7 @@ pub struct PersistentNotification { } #[rmc_proto(102)] -pub trait Friends { +pub trait FriendsWiiU { #[method_id(1)] async fn update_and_get_all_information( &self, diff --git a/rnex-core/src/rmc/protocols/mod.rs b/rnex-core/src/rmc/protocols/mod.rs index 411ae28..6f14323 100644 --- a/rnex-core/src/rmc/protocols/mod.rs +++ b/rnex-core/src/rmc/protocols/mod.rs @@ -3,7 +3,8 @@ pub mod account_management; pub mod auth; pub mod datastore; -pub mod friends; +pub mod friends_3ds; +pub mod friends_wiiu; pub mod matchmake; pub mod matchmake_ext; pub mod matchmake_extension; diff --git a/rnex-core/src/server_api/gatherings.rs b/rnex-core/src/server_api/gatherings.rs new file mode 100644 index 0000000..735b34f --- /dev/null +++ b/rnex-core/src/server_api/gatherings.rs @@ -0,0 +1,88 @@ +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, +}; + +use rnex_server_api::gatherings::{ + Gatherings, gathering_info_service_server::GatheringInfoService, +}; +use tonic::{Request, Response, Status, async_trait}; + +use crate::{ + nex::matchmake::{ExtendedMatchmakeSession, MatchmakeManager}, + rmc::structures::matchmake::{self, MatchmakeSession}, + server_api, +}; + +impl Into for &ExtendedMatchmakeSession { + fn into(self) -> rnex_server_api::gatherings::Gathering { + let players = self.get_active_players().map(|p| *p.pid as u64).collect(); + let ExtendedMatchmakeSession { + session: + MatchmakeSession { + gathering: + matchmake::Gathering { + state, + flags, + host_pid, + description, + maximum_participants, + minimum_participants, + owner_pid, + participant_policy, + policy_argument, + self_gid, + }, + application_buffer, + attributes, + gamemode, + matchmake_system_type, + open_participation, + participation_count, + session_key, + }, + connected_players, + } = self; + rnex_server_api::gatherings::Gathering { + players, + description: description.clone(), + flags: *flags, + host_pid: *host_pid as _, + maximum_participants: *maximum_participants as _, + minimum_participants: *minimum_participants as _, + owner_pid: *owner_pid as _, + participant_policy: *participant_policy, + policy_argument: *policy_argument, + self_gid: *self_gid, + state: *state, + application_buffer: application_buffer.clone(), + attributes: attributes.clone(), + ..Default::default() + } + } +} + +struct GatheringsApi(Arc); + +#[async_trait] +impl GatheringInfoService for GatheringsApi { + async fn get_gatherings( + &self, + request: Request<()>, + ) -> std::result::Result, Status> { + Ok(Response::new(Gatherings { + gatherings: self + .0 + .sessions + .read() + .await + .keys() + .map(|gid| { + let mut hasher = DefaultHasher::new(); + gid.hash(&mut hasher); + hasher.finish() + }) + .collect(), + })) + } +} diff --git a/rnex-core/src/server_api/meta.rs b/rnex-core/src/server_api/meta.rs new file mode 100644 index 0000000..fb5b5ef --- /dev/null +++ b/rnex-core/src/server_api/meta.rs @@ -0,0 +1,36 @@ +use rnex_server_api::meta::{ + ApiFeature, ApiFeatures, BuildInfo, server_meta_service_server::ServerMetaService, +}; +use tonic::{Request, Response, Status, async_trait, transport::Server}; + +pub struct ServerMeta; + +#[async_trait] +impl ServerMetaService for ServerMeta { + async fn get_build_info(&self, request: Request<()>) -> Result, Status> { + Ok(Response::new(BuildInfo { + version: env!("CARGO_PKG_VERSION").to_owned(), + edition: env!("EDITION").to_owned(), + build_hash: env!("GIT_HASH").to_owned(), + feature_set: env!("FEATURESET").to_owned(), + })) + } + async fn get_api_features( + &self, + request: Request<()>, + ) -> Result, Status> { + let mut api_features = vec![ApiFeature { + name: "meta".to_owned(), + needs_admin: false, + version: 0, + }]; + + #[cfg(not(feature = "friends"))] + api_features.push(ApiFeature { + name: "gatherings".to_owned(), + version: 0, + needs_admin: true, + }); + Ok(Response::new(ApiFeatures { api_features })) + } +} diff --git a/rnex-core/src/server_api/mod.rs b/rnex-core/src/server_api/mod.rs new file mode 100644 index 0000000..b1389c8 --- /dev/null +++ b/rnex-core/src/server_api/mod.rs @@ -0,0 +1,22 @@ +use std::{net::SocketAddr, str::FromStr}; + +use rnex_server_api::meta::server_meta_service_server::ServerMetaServiceServer; +use tonic::transport::Server; + +#[cfg(not(feature = "friends"))] +use crate::nex::matchmake::MatchmakeManager; +use crate::server_api::meta::ServerMeta; +#[cfg(not(feature = "friends"))] +use std::sync::Arc; +#[cfg(not(feature = "friends"))] +mod gatherings; + +mod meta; + +pub async fn launch_server_api(#[cfg(not(feature = "friends"))] mmm: Arc) { + let mut server = Server::builder().add_service(ServerMetaServiceServer::new(ServerMeta)); + + server + .serve(SocketAddr::from_str("0.0.0.0:80").expect("unable to make sockaddr from ip")) + .await; +} diff --git a/rnex-server-api-grpc/gatherings.proto b/rnex-server-api-grpc/gatherings.proto new file mode 100644 index 0000000..cef3a90 --- /dev/null +++ b/rnex-server-api-grpc/gatherings.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; +// NOTE: NEVER give out gids to the general public, people can and will be +// able to join random mkatches which they arent supposed to join via them +// +// As an alternative use a unique hash of the Gid + +import "google/protobuf/empty.proto"; + +package gatherings; + +message MatchmakeParam{ + +} + +message KerberosDateTime{ + uint32 raw = 1; +} + +message Gathering{ + repeated uint64 players = 1; + uint32 self_gid = 2; + uint64 owner_pid = 3; + uint64 host_pid = 4; + uint32 minimum_participants = 5; + uint32 maximum_participants = 6; + uint32 participant_policy = 7; + uint32 policy_argument = 8; + uint32 flags = 9; + uint32 state = 10; + string description = 11; + uint32 gamemode = 12; + repeated uint32 attributes = 13; + bool open_participation = 14; + uint32 matchmake_system_type = 15; + bytes application_buffer = 16; + uint32 participation_count = 17; + bytes session_key = 18; + optional uint32 progress_score = 19; + optional uint32 option0 = 20; + optional MatchmakeParam matchmake_param = 21; + optional KerberosDateTime datetime = 22; + optional string user_password = 23; + optional uint32 refer_gid = 24; + optional bool user_password_enabled = 25; + optional bool system_password_enabled = 26; +} + +message Gatherings{ + repeated Gathering gatherings = 1; +} + +service GatheringInfoService{ + rpc GetGatherings(google.protobuf.Empty) returns (Gatherings); +} diff --git a/rnex-server-api-grpc/meta.proto b/rnex-server-api-grpc/meta.proto new file mode 100644 index 0000000..e256e62 --- /dev/null +++ b/rnex-server-api-grpc/meta.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package meta; + +import "google/protobuf/empty.proto"; + +message BuildInfo { + string version = 1; + string edition = 2; + string feature_set = 3; + string build_hash = 4; +} + +message ApiFeature { + string name = 1; + int32 version = 2; + bool needs_admin = 3; +} + +message ApiFeatures { + repeated ApiFeature api_features = 1; +} + +service ServerMetaService { + rpc GetBuildInfo(google.protobuf.Empty) returns (BuildInfo); + rpc GetApiFeatures(google.protobuf.Empty) returns (ApiFeatures); +} diff --git a/rnex-server-api/Cargo.toml b/rnex-server-api/Cargo.toml new file mode 100644 index 0000000..c10be01 --- /dev/null +++ b/rnex-server-api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rnex-server-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +prost = "0.14.3" +tonic-prost = "*" +tonic = "0.14.6" + +[build-dependencies] +tonic-prost-build = "0.14.6" diff --git a/rnex-server-api/build.rs b/rnex-server-api/build.rs new file mode 100644 index 0000000..4c9a954 --- /dev/null +++ b/rnex-server-api/build.rs @@ -0,0 +1,24 @@ +use std::{env, fs}; + +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../rnex-server-api-grpc/"); + + let protos: Vec = fs::read_dir("../rnex-server-api-grpc/")? + .filter(|e| { + e.as_ref().is_ok_and(|e| { + e.file_name() + .to_str() + .is_some_and(|n| n.ends_with(".proto")) + }) + }) + .filter_map(|e| { + e.ok() + .map(|v| v.path().to_str().map(|v| v.to_owned())) + .flatten() + }) + .collect(); + tonic_prost_build::configure() + .compile_protos(&protos[..], &["../rnex-server-api-grpc/".to_owned()])?; + Ok(()) +} diff --git a/rnex-server-api/src/lib.rs b/rnex-server-api/src/lib.rs new file mode 100644 index 0000000..c1a3fed --- /dev/null +++ b/rnex-server-api/src/lib.rs @@ -0,0 +1,6 @@ +pub mod meta { + tonic::include_proto!("meta"); +} +pub mod gatherings { + tonic::include_proto!("gatherings"); +}