progress
Some checks failed
Build and Test / super-mario-maker (push) Failing after 10m30s
Build and Test / fast-racing-neo (push) Failing after 11m40s
Build and Test / friends (push) Failing after 11m56s
Build and Test / wii-sports-club (push) Failing after 12m9s
Build and Test / splatoon (push) Failing after 12m14s
Build and Test / puyopuyo (push) Failing after 12m23s
Build and Test / wii-u-chat (push) Failing after 12m23s
Build and Test / mario-tennis (push) Failing after 12m23s
Build and Test / sonic-transformed (push) Failing after 12m23s
Build and Test / splatoon-testfire (push) Failing after 12m30s
Build and Test / minecraft-wiiu (push) Failing after 12m37s

This commit is contained in:
Maple Nebel 2026-07-12 18:21:18 +02:00
commit 4ff0a5efcc
187 changed files with 4526 additions and 5132 deletions

View file

@ -0,0 +1,39 @@
[package]
name = "rnex-auth"
version = "0.1.0"
edition = "2024"
[dependencies]
rnex-rmc = { path = "../../rnex-rmc" }
rnex-auth-protos = { path = "../../rnex-protocols/auth-protos" }
rnex-reggie-protos = { path = "../../rnex-protocols/reggie-protos" }
rnex-server = { path = "../../rnex-server" }
rnex-prudp = { path = "../../rnex-prudp" }
hex = "0.4.3"
rand = "0.10.2"
nex-account = { version = "0.2.4", registry = "spbr" }
tonic = "0.14.6"
tracing = "0.1.44"
cfg-if = "1.0.4"
anyhow = "1.0.103"
tokio = { version = "1.52.3", features = ["net"] }
[features]
rmc_struct_header = []
guest_login = []
friends = ["guest_login", "database-support"]
big_pid = []
third-notif-param = []
v3-3-2 = []
v3-4-0 = ["v3-3-2", "third-notif-param", "rmc_struct_header"]
v3-5-0 = ["v3-4-0"]
v3-8-15 = ["v3-5-0"]
v3-10-22 = ["v3-8-15"]
v4-3-11 = ["v3-8-15"]
nx = ["big_pid"]
splatoon = ["v3-5-0"]
datastore = ["database-support", "v3-8-15"]
database-support = []
[lints]
workspace = true

View file

@ -0,0 +1,354 @@
use std::{
hash::{DefaultHasher, Hasher},
net::SocketAddrV4,
sync::{Arc, LazyLock},
};
use cfg_if::cfg_if;
use nex_account::{
grpc::{self, nex_account_service_client},
grpc_client,
};
use rnex_auth_protos::{
LocalAuthProtocol,
auth::{Auth, ConnectionData, ConnectionDataOld},
};
use rnex_prudp::kerberos::{Ticket, TicketInternalData};
use rnex_reggie_protos::reggie::{RemoteEdgeNodeHolder, RemoteEdgeNodeManagement};
use rnex_rmc::{
OnlyRemote,
any::Any,
define_rmc_proto,
qresult::QResult,
rand,
response::ErrorCode,
rmc_struct,
util::{PID, account::Account, date_time::DateTime},
};
use rnex_server::PassthroughInitModule;
use tracing::{info, warn};
use crate::AuthManager;
#[derive(Debug)]
#[rmc_struct(AuthProtocol)]
pub struct AuthHandler {
pub(crate) am: PassthroughInitModule<AuthManager>,
}
pub fn generate_ticket(
source_act_login_data: (PID, [u8; 16]),
dest_act_login_data: (PID, [u8; 16]),
) -> Box<[u8]> {
let source_key = source_act_login_data.1;
let dest_key = dest_act_login_data.1;
let internal_data = TicketInternalData::new(source_act_login_data.0);
let encrypted_inner = internal_data.encrypt(dest_key);
Ticket {
pid: dest_act_login_data.0,
session_key: internal_data.session_key,
}
.encrypt(source_key, &encrypted_inner)
}
pub fn generate_ticket_with_string_user_key(
source_act: PID,
dest_act_login_data: (PID, [u8; 16]),
) -> (String, Box<[u8]>) {
let source_key: [u8; 8] = rand::random();
let key_string = hex::encode(source_key);
let key_data: [u8; 16] = key_string.as_bytes().try_into().unwrap();
let dest_key = dest_act_login_data.1;
let internal_data = TicketInternalData::new(source_act);
let encrypted_inner = internal_data.encrypt(dest_key);
let encrypted_session_ticket = Ticket {
pid: dest_act_login_data.0,
session_key: internal_data.session_key,
}
.encrypt(key_data, &encrypted_inner);
(key_string, encrypted_session_ticket)
}
async fn get_login_data_by_pid(pid: PID) -> Option<(PID, [u8; 16])> {
if pid == GUEST_ACCOUNT.pid {
let source_login_data = GUEST_ACCOUNT.get_login_data();
return Some((source_login_data.0, source_login_data.1));
}
let Ok(mut client) = nex_account::grpc_client().await else {
return None;
};
let Ok(passwd) = client.get_nex_key_by_pid(grpc::Pid { pid }).await else {
return None;
};
let passwd = passwd.into_inner().key.try_into().ok()?;
Some((pid, passwd))
}
fn station_url_from_sock_addr(sock_addr: SocketAddrV4) -> String {
format!(
"prudps:/PID=2;sid=1;stream=10;type=2;address={};port={};CID=1",
sock_addr.ip(),
sock_addr.port()
)
}
static GUEST_ACCOUNT: LazyLock<Account> =
LazyLock::new(|| Account::new(100, "guest", "MMQea3n!fsik"));
impl AuthHandler {
pub async fn generate_ticket_from_name(
&self,
name: &str,
) -> Result<(PID, Box<[u8]>), ErrorCode> {
#[cfg(feature = "guest_login")]
{
if name == GUEST_ACCOUNT.username {
info!("guest account login");
let source_login_data = GUEST_ACCOUNT.get_login_data();
let destination_login_data = self.am.destination_server_acct.get_login_data();
return Ok((
source_login_data.0,
generate_ticket(source_login_data, destination_login_data),
));
}
}
info!("parsing pid");
let Ok(pid) = name.parse() else {
warn!("unable to connect to parse pid: {}", name);
return Err(ErrorCode::Core_InvalidArgument);
};
info!("creating account grpc client");
let Ok(mut client) = grpc_client().await else {
warn!("unable to connect to grpc");
return Err(ErrorCode::Core_Exception);
};
info!("grabbing nex key");
let Ok(passwd) = client.get_nex_key_by_pid(grpc::Pid { pid }).await else {
warn!("unable to get nex password for pid: {}:", pid);
return Err(ErrorCode::Core_Exception);
};
let passwd = passwd
.into_inner()
.key
.try_into()
.map_err(|_| ErrorCode::RendezVous_InvalidPassword)?;
info!("source login data");
let source_login_data = (pid, passwd);
println!("{}, {:?}", pid, passwd);
let destination_login_data = self.am.destination_server_acct.get_login_data();
info!("we are a-ok here");
Ok((
pid,
generate_ticket(source_login_data, destination_login_data),
))
}
pub fn generate_ticket_from_name_string_user_key(
&self,
name: &str,
) -> Result<(PID, String, Box<[u8]>), ErrorCode> {
{
if name == GUEST_ACCOUNT.username {
let source_login_data = GUEST_ACCOUNT.get_login_data();
let destination_login_data = self.am.destination_server_acct.get_login_data();
let ticket = generate_ticket_with_string_user_key(
source_login_data.0,
destination_login_data,
);
return Ok((source_login_data.0, ticket.0, ticket.1));
}
}
let Ok(pid) = name.parse() else {
warn!("unable to connect to parse pid: {}", name);
return Err(ErrorCode::Core_InvalidArgument);
};
let destination_login_data = self.am.destination_server_acct.get_login_data();
let data = generate_ticket_with_string_user_key(pid, destination_login_data);
Ok((pid, data.0, data.1))
}
}
impl Auth for AuthHandler {
async fn login(
&self,
name: String,
) -> Result<(QResult, PID, Vec<u8>, ConnectionDataOld, String), ErrorCode> {
let (pid, ticket) = self.generate_ticket_from_name(&name).await?;
let result = QResult::success(ErrorCode::Core_Unknown);
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
let Ok(addr) = self.am.control_server.get_url(hasher.finish()).await else {
warn!("no secure proxies");
return Err(ErrorCode::Core_Exception);
};
let connection_data = ConnectionDataOld {
station_url: station_url_from_sock_addr(addr),
special_station_url: "".to_string(),
special_protocols: Vec::new(),
};
let ret = (
result,
pid,
ticket.into(),
connection_data,
self.am.build_name.to_string(),
);
info!("data: {:?}", ret);
Ok(ret)
}
cfg_if! {
if #[cfg(feature = "nx")]{
async fn login_ex(
&self,
name: String,
_extra_data: Any,
) -> Result<(QResult, PID, Vec<u8>, ConnectionData, String, String), ErrorCode> {
let (pid, key, ticket) = self.generate_ticket_from_name_string_user_key(&name).await?;
let result = QResult::success(Core_Unknown);
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
let Ok(addr) = self.control_server.get_url(hasher.finish()).await else {
warn!("no secure proxies");
return Err(ErrorCode::Core_Exception);
};
let connection_data = ConnectionData {
station_url: station_url_from_sock_addr(addr),
special_station_url: "".to_string(),
//date_time: KerberosDateTime::new(1,1,1,1,1,1),
date_time: KerberosDateTime::now(),
special_protocols: Vec::new(),
};
let ret = (
result,
pid,
ticket.into(),
connection_data,
self.build_name.to_string(),
key
);
info!("data: {:?}", ret);
Ok(ret)
}
async fn request_ticket(
&self,
source_pid: PID,
destination_pid: PID,
) -> Result<(QResult, Vec<u8>, String), ErrorCode> {
let Some((pid, _)) = get_login_data_by_pid(source_pid).await else {
return Err(ErrorCode::Core_Exception);
};
let desgination_login_data = if destination_pid == self.destination_server_acct.pid {
self.destination_server_acct.get_login_data()
} else {
return Err(ErrorCode::RendezVous_InvalidOperation);
};
let result = QResult::success(Core_Unknown);
let ticket = generate_ticket_with_string_user_key(pid, desgination_login_data);
Ok((result, ticket.1.into(), ticket.0))
}
} else {
async fn login_ex(
&self,
name: String,
_extra_data: Any,
) -> Result<(QResult, PID, Vec<u8>, ConnectionData, String), ErrorCode> {
let (pid, ticket) = self.generate_ticket_from_name(&name).await?;
let result = QResult::success(ErrorCode::Core_Unknown);
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
let Ok(addr) = self.am.control_server.get_url(hasher.finish()).await else {
warn!("no secure proxies");
return Err(ErrorCode::Core_Exception);
};
let connection_data = ConnectionData {
station_url: station_url_from_sock_addr(addr),
special_station_url: "".to_string(),
//date_time: KerberosDateTime::new(1,1,1,1,1,1),
date_time: DateTime::now(),
special_protocols: Vec::new(),
};
let ret = (
result,
pid,
ticket.into(),
connection_data,
self.am.build_name.to_string(),
);
info!("data: {:?}", ret);
Ok(ret)
}
async fn request_ticket(
&self,
source_pid: PID,
destination_pid: PID,
) -> Result<(QResult, Vec<u8>), ErrorCode> {
let Some((pid, passwd)) = get_login_data_by_pid(source_pid).await else {
return Err(ErrorCode::Core_Exception);
};
let desgination_login_data = if destination_pid == self.am.destination_server_acct.pid {
self.am.destination_server_acct.get_login_data()
} else {
return Err(ErrorCode::RendezVous_InvalidOperation);
};
let result = QResult::success(ErrorCode::Core_Unknown);
let ticket = generate_ticket((pid, passwd), desgination_login_data);
Ok((result, ticket.into()))
}
}
}
async fn get_pid(&self, _username: String) -> Result<u32, ErrorCode> {
Err(ErrorCode::Core_Exception)
}
async fn get_name(&self, _pid: PID) -> Result<String, ErrorCode> {
Err(ErrorCode::Core_Exception)
}
}

View file

@ -0,0 +1,87 @@
#[allow(async_fn_in_trait)]
pub mod auth_handler;
use std::{convert::Infallible, env, error::Error, net::SocketAddr, sync::Arc};
use tokio::net::TcpStream;
use nex_account::{grpc::Pid, grpc_client};
use rnex_reggie_protos::reggie::{EdgeNodeHolderConnectOption::DontRegister, RemoteEdgeNodeHolder};
use rnex_rmc::{
OnlyRemote, new_rmc_gateway_connection,
serialization::RmcSerialize,
util::{SplittableBufferConnection, account::Account},
};
use rnex_server::{RnexManager, RnexModule, WeakPassthroughInitModule};
use tracing::info;
use crate::auth_handler::AuthHandler;
#[derive(Debug)]
pub struct AuthManager {
pub destination_server_acct: Account,
pub build_name: &'static str,
pub control_server: Arc<OnlyRemote<RemoteEdgeNodeHolder>>,
}
#[derive(Debug)]
pub struct AuthModule;
impl RnexManager for AuthManager {
type User = AuthHandler;
type InitData = SocketAddr;
async fn init_new_user(
this: rnex_server::PassthroughInitModule<Self>,
_: &rnex_server::ModuleHolder,
_: &rnex_rmc::RmcConnection,
init_data: &Self::InitData,
_: WeakPassthroughInitModule<Self::User>,
) -> Self::User {
info!(target: "proxy_connections", address = ?init_data, "user connected");
Self::User { am: this }
}
}
/*
pub static FORWARD_EDGE_NODE_HOLDER: Lazy<SocketAddrV4> = Lazy::new(|| {
env::var("FORWARD_EDGE_NODE_HOLDER")
.ok()
.and_then(|s| Some(s.parse().unwrap()))
.expect("FORWARD_EDGE_NODE_HOLDER not set")
});*/
impl RnexModule for AuthModule {
type Manager = AuthManager;
type InitError = anyhow::Error;
async fn create_manager(
_: &rnex_server::ModuleHolder,
) -> Result<Self::Manager, Self::InitError> {
let conn = TcpStream::connect(env::var("FORWARD_EDGE_NODE_HOLDER")?)
.await
.unwrap();
let conn: SplittableBufferConnection = conn.into();
conn.send(DontRegister.to_data().unwrap()).await;
let conn = new_rmc_gateway_connection(conn, async |r| {
Arc::new(OnlyRemote::<RemoteEdgeNodeHolder>::new(r))
})
.await;
Ok(AuthManager {
build_name: option_env!("AUTH_REPORT_VERSION").unwrap_or("no version specified"),
control_server: conn,
// todo: update nex-account to allow pulling the entire account info for rnex
destination_server_acct: Account::new_raw_key(
2,
"Quazal Rendez-Vous",
grpc_client()
.await?
.get_nex_key_by_pid(Pid { pid: 2 })
.await?
.into_inner()
.key
.try_into()
.map_err(|_| anyhow::Error::msg("invalid key size"))?,
),
})
}
}

View file

@ -0,0 +1,17 @@
[package]
name = "rnex-base"
version = "0.1.0"
edition = "2024"
[dependencies]
rnex-rmc = { path = "../../rnex-rmc" }
rnex-util = { path = "../../rnex-util" }
rnex-base-protos = { path = "../../rnex-protocols/base-protos" }
rnex-server = { path = "../../rnex-server" }
tokio = { version = "1.52.3", features = ["sync"] }
rand = "0.10.2"
tracing = "0.1.44"
cfg-if = "1.0.4"
[lints]
workspace = true

View file

@ -0,0 +1,52 @@
use std::{
convert::Infallible,
sync::atomic::{AtomicU32, Ordering::Relaxed},
};
use rnex_server::{ConnectionInitData, RnexManager, RnexModule, WeakPassthroughInitModule};
use crate::user::BaseUser;
pub mod user;
#[derive(Default, Debug)]
pub struct BaseManager {
cid_counter: AtomicU32,
}
#[derive(Debug, Default)]
pub struct BaseModule;
impl RnexManager for BaseManager {
type User = BaseUser;
type InitData = ConnectionInitData;
async fn init_new_user(
this: rnex_server::PassthroughInitModule<Self>,
_mod_holder: &rnex_server::ModuleHolder,
_remote: &rnex_rmc::RmcConnection,
init_data: &Self::InitData,
_: WeakPassthroughInitModule<Self::User>,
) -> Self::User {
BaseUser {
cid: this.cid_counter.fetch_add(1, Relaxed),
bm: this,
addr: init_data.addr,
pid: init_data.pid,
station_url: Default::default(),
}
}
}
impl RnexModule for BaseModule {
type Manager = BaseManager;
type InitError = Infallible;
async fn create_manager(
_mod_holder: &rnex_server::ModuleHolder,
) -> Result<Self::Manager, Self::InitError> {
Ok(BaseManager::default())
}
}

View file

@ -0,0 +1,198 @@
use std::net::SocketAddr;
use rnex_base_protos::{LocalBaseProtocol, secure::Secure, util::Utility};
use rnex_rmc::{any::Any, qresult::QResult, response::ErrorCode, rmc_struct};
use rnex_server::PassthroughInitModule;
use rnex_util::{
PID,
station_url::{StationUrl, UrlOptions, nat_types::PUBLIC},
};
use tokio::sync::{Mutex, RwLock};
use tracing::info;
use crate::BaseManager;
pub async fn get_station_urls(
station_urls: &[StationUrl],
addr: SocketAddr,
pid: PID,
cid: u32,
) -> Result<Vec<StationUrl>, ErrorCode> {
let mut public_station: Option<StationUrl> = None;
let mut private_station: Option<StationUrl> = None;
for station in station_urls {
let is_public = station.options.iter().any(|v| {
if let UrlOptions::NatType(v) = v
&& *v & PUBLIC != 0
{
return true;
}
false
});
let Some(nat_filtering) = station.options.iter().find_map(|v| match v {
UrlOptions::NatFiltering(v) => Some(v),
_ => None,
}) else {
return Err(ErrorCode::Core_InvalidArgument);
};
let Some(nat_mapping) = station.options.iter().find_map(|v| match v {
UrlOptions::NatMapping(v) => Some(v),
_ => None,
}) else {
return Err(ErrorCode::Core_InvalidArgument);
};
if !is_public || (*nat_filtering == 0 && *nat_mapping == 0) {
private_station = Some(station.clone());
}
if is_public {
public_station = Some(station.clone());
}
}
let Some(mut private_station) = private_station else {
return Err(ErrorCode::Core_InvalidArgument);
};
let mut public_station = if let Some(public_station) = public_station {
public_station
} else {
let mut public_station = private_station.clone();
public_station.options.retain(|v| {
!matches!(
v,
UrlOptions::Address(_)
| UrlOptions::Port(_)
| UrlOptions::NatFiltering(_)
| UrlOptions::NatMapping(_)
| UrlOptions::NatType(_)
)
});
public_station.options.push(UrlOptions::Address(addr.ip()));
public_station.options.push(UrlOptions::Port(addr.port()));
public_station.options.push(UrlOptions::NatFiltering(0));
public_station.options.push(UrlOptions::NatMapping(0));
public_station.options.push(UrlOptions::NatType(3));
public_station
};
let both = [&mut public_station, &mut private_station];
for station in both {
station.options.retain(|v| {
!matches!(
v,
UrlOptions::PrincipalID(_)
| UrlOptions::RVConnectionID(_)
| UrlOptions::ConnectionID(_)
)
});
station.options.push(UrlOptions::PrincipalID(pid));
station.options.push(UrlOptions::RVConnectionID(cid));
station.options.push(UrlOptions::ConnectionID(cid));
}
Ok(vec![public_station])
}
#[rmc_struct(BaseProtocol)]
#[derive(Debug)]
pub struct BaseUser {
pub(crate) bm: PassthroughInitModule<BaseManager>,
pub addr: SocketAddr,
pub station_url: RwLock<Vec<StationUrl>>,
pub pid: PID,
pub cid: u32,
}
impl Secure for BaseUser {
async fn register(
&self,
station_urls: Vec<StationUrl>,
) -> Result<(QResult, u32, StationUrl), ErrorCode> {
let cid = self.cid;
println!("{:?}", station_urls);
/*
let mut users = self.matchmake_manager.users.write().await;
users.insert(cid, self.this.clone());
drop(users);
let mut users = self.matchmake_manager.users_by_pid.write().await;
users.insert(self.pid, self.this.clone());
drop(users);
*/
let stations = get_station_urls(&station_urls, self.addr, self.pid, cid).await?;
let first = stations.first().unwrap().clone();
let mut lock = self.station_url.write().await;
*lock = stations;
drop(lock);
Ok((QResult::success(ErrorCode::Core_Unknown), self.cid, first))
}
async fn register_ex(
&self,
station_urls: Vec<StationUrl>,
_data: Any,
) -> Result<(QResult, u32, StationUrl), ErrorCode> {
self.register(station_urls).await
}
async fn replace_url(&self, target_url: StationUrl, dest: StationUrl) -> Result<(), ErrorCode> {
let mut lock = self.station_url.write().await;
info!("target URL: {:?}", target_url);
info!("dest URL: {:?}", dest);
let Some(target_addr) = target_url
.options
.iter()
.find(|v| matches!(v, UrlOptions::Address(_)))
else {
return Err(ErrorCode::Core_InvalidArgument);
};
let Some(target_port) = target_url
.options
.iter()
.find(|v| matches!(v, UrlOptions::Port(_)))
else {
return Err(ErrorCode::Core_InvalidArgument);
};
let Some(replacement_target) = lock.iter_mut().find(|url| {
url.options.iter().any(|o| o == target_addr)
&& url.options.iter().any(|o| o == target_port)
}) else {
//probably internal ip
return Ok(());
};
*replacement_target = dest;
drop(lock);
Ok(())
}
}
impl Utility for BaseUser {
async fn acquire_nex_unique_id(&self) -> Result<u64, ErrorCode> {
return Ok(rand::random());
}
async fn get_integer_settings(&self, _index: u32) -> Result<Vec<(u16, i32)>, ErrorCode> {
Ok(vec![(0, 1), (1, 2), (2, 0), (3, 4)])
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "rnex-ds"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = true

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,9 @@
[package]
name = "rnex-fpd"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = true

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,38 @@
[package]
name = "rnex-mm"
version = "0.1.0"
edition = "2024"
[dependencies]
rnex-rmc = { path = "../../rnex-rmc" }
rnex-util = { path = "../../rnex-util" }
rnex-mm-protos = { path = "../../rnex-protocols/mm-protos" }
rnex-base-protos = { path = "../../rnex-protocols/base-protos" }
rnex-server = { path = "../../rnex-server" }
rnex-base = { path = "../rnex-base" }
tokio = { version = "1.52.3", features = ["sync"] }
rand = "0.10.2"
tracing = "0.1.44"
cfg-if = "1.0.4"
bytemuck = "1.25.1"
serde = { version = "1.0.228", features = ["derive"] }
ureq = "3.3.0"
[features]
rmc_struct_header = []
guest_login = []
friends = ["guest_login", "database-support"]
big_pid = []
third-notif-param = []
v3-3-2 = []
v3-4-0 = ["v3-3-2", "third-notif-param", "rmc_struct_header"]
v3-5-0 = ["v3-4-0"]
v3-8-15 = ["v3-5-0"]
v3-10-22 = ["v3-8-15"]
v4-3-11 = ["v3-8-15"]
nx = ["big_pid"]
splatoon = ["v3-5-0"]
datastore = ["database-support", "v3-8-15"]
database-support = []
[lints]
workspace = true

View file

@ -0,0 +1,25 @@
use std::{convert::Infallible, sync::atomic::AtomicU32};
use rnex_server::RnexModule;
use crate::matchmake::MatchmakeManager;
pub mod matchmake;
pub mod user;
pub struct MatchMakeModule;
impl RnexModule for MatchMakeModule {
type Manager = MatchmakeManager;
type InitError = Infallible;
async fn create_manager(
_: &rnex_server::ModuleHolder,
) -> Result<Self::Manager, Self::InitError> {
Ok(MatchmakeManager {
sessions: Default::default(),
rv_cid_counter: AtomicU32::new(1),
users: Default::default(),
users_by_pid: Default::default(),
})
}
}

View file

@ -0,0 +1,619 @@
use rnex_mm_protos::{RemoteMatchMakingClientProtocol, notifications::RemoteNotification};
use std::{
collections::HashMap,
str::FromStr,
sync::{
Arc, Weak,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use cfg_if::cfg_if;
use rand::random;
use rnex_mm_protos::{
matchmake::{
Gathering, MatchmakeSession, MatchmakeSessionSearchCriteria,
gathering_flags::PERSISTENT_GATHERING,
},
notifications::{
NotificationEvent,
notification_types::{HOST_CHANGED, OWNERSHIP_CHANGED},
},
};
use rnex_rmc::{RmcPureRemoteObject, response::ErrorCode};
use rnex_server::{
ConnectionInitData, PassthroughInitModule, RnexManager, WeakPassthroughInitModule,
};
use rnex_util::PID;
use tokio::{
sync::{Mutex, RwLock},
time::sleep,
};
use tracing::{info, instrument};
use crate::user::MatchmakeUser;
#[derive(Debug)]
pub struct MatchmakeManager {
//pub gid_counter: AtomicU32,
pub sessions: RwLock<HashMap<u32, Arc<Mutex<ExtendedMatchmakeSession>>>>,
pub rv_cid_counter: AtomicU32,
pub users: RwLock<HashMap<u32, WeakPassthroughInitModule<MatchmakeUser>>>,
pub users_by_pid: RwLock<HashMap<PID, WeakPassthroughInitModule<MatchmakeUser>>>,
}
impl MatchmakeManager {
pub fn next_gid(&self) -> u32 {
random()
//self.gid_counter.fetch_add(1, Relaxed)
}
pub fn next_cid(&self) -> u32 {
self.rv_cid_counter.fetch_add(1, Ordering::Relaxed)
}
#[instrument]
pub async fn get_session(
&self,
gid: u32,
) -> Result<Arc<Mutex<ExtendedMatchmakeSession>>, ErrorCode> {
let sessions = self.sessions.read().await;
let Some(session) = sessions.get(&gid) else {
return Err(ErrorCode::RendezVous_SessionVoid);
};
let session = session.clone();
drop(sessions);
Ok(session)
}
#[instrument]
async fn garbage_collect(&self) {
info!("running rnex garbage collector over all sessions and users");
let mut idx = 0;
let mut to_be_deleted_gids = Vec::new();
// i am very well aware of how inefficient doing it like this is but this is the only
// way which i could think of to do this without potentially causing a deadlock of
// the entire server
while let Some((gid, session)) = {
let sessions = self.sessions.read().await;
let session_pair = sessions.iter().nth(idx).map(|s| (*s.0, s.1.clone()));
drop(sessions);
session_pair
} {
let session = session.lock().await;
if !session.is_reachable() {
to_be_deleted_gids.push(gid);
}
idx += 1;
}
let mut sessions = self.sessions.write().await;
for gid in to_be_deleted_gids {
sessions.remove(&gid);
}
}
#[instrument]
pub fn initialize_garbage_collect_thread(this: Weak<Self>) {
tokio::spawn(async move {
while let Some(this) = this.upgrade() {
this.garbage_collect().await;
// every 5 minutes
sleep(Duration::from_secs(60 * 5)).await;
}
});
}
// this could be far more efficient but it is INCREDIBLY difficult to iterate over something
// asyncronously propperly
#[instrument]
pub async fn search_by_criteria(
&self,
criterias: &[MatchmakeSessionSearchCriteria],
) -> Result<Vec<Arc<Mutex<ExtendedMatchmakeSession>>>, ErrorCode> {
let sessions = self.sessions.read().await;
let mut list = Vec::with_capacity(sessions.len());
for session in sessions.values() {
let inner_session = session.lock().await;
if !inner_session.is_joinable() {
continue;
}
let mut bool_matched_criteria = false;
for criteria in criterias {
if inner_session.matches_criteria(criteria)? {
bool_matched_criteria = true;
}
}
if bool_matched_criteria {
println!("matched session: {:?}", session);
list.push(session.clone());
}
}
drop(sessions);
Ok(list)
}
}
#[derive(Default, Debug)]
pub struct ExtendedMatchmakeSession {
pub session: MatchmakeSession,
pub connected_players: Vec<WeakPassthroughInitModule<MatchmakeUser>>,
}
fn read_bounds_string<T: FromStr>(str: &str) -> Option<(T, T)> {
let bounds = str.split_once(",")?;
Some((T::from_str(bounds.0).ok()?, T::from_str(bounds.1).ok()?))
}
fn check_bounds_str<T: FromStr + PartialOrd>(compare: T, str: &str) -> Option<bool> {
if let Some(bounds) = read_bounds_string::<T>(str) {
return Some(bounds.0 <= compare && compare <= bounds.1);
}
if let Ok(val) = T::from_str(str) {
return Some(val == compare);
}
if str.is_empty() {
return Some(true);
}
None
}
pub async fn broadcast_notification<T: AsRef<MatchmakeUser>>(
players: impl Iterator<Item = T>,
notification_event: &NotificationEvent,
) {
for player in players {
let player = player.as_ref();
player
.remote
.process_notification_event(notification_event.clone())
.await;
}
}
impl ExtendedMatchmakeSession {
#[inline(always)]
pub fn get_active_players(&self) -> impl Iterator<Item = PassthroughInitModule<MatchmakeUser>> {
self.connected_players.iter().filter_map(|u| u.upgrade())
}
#[inline(always)]
pub async fn broadcast_notification(&self, notification_event: &NotificationEvent) {
broadcast_notification(self.get_active_players(), notification_event).await;
}
pub async fn from_matchmake_session(
gid: u32,
session: MatchmakeSession,
host: &WeakPassthroughInitModule<MatchmakeUser>,
) -> Self {
let Some(host) = host.upgrade() else {
return Default::default();
};
cfg_if! {
if #[cfg(feature = "v3-5-0")]{
let mm_session = MatchmakeSession {
gathering: Gathering {
self_gid: gid,
owner_pid: host.pid,
host_pid: host.pid,
..session.gathering.clone()
},
datetime: DateTime::now(),
session_key: (0..32).map(|_| random()).collect(),
matchmake_param: MatchmakeParam {
params: vec![
("@SR".to_owned(), Variant::Bool(true)),
("@GIR".to_owned(), Variant::SInt64(3)),
],
},
system_password_enabled: false,
..session
};
return Self {
session: mm_session,
connected_players: Default::default(),
}
} else {
let mm_session = MatchmakeSession {
gathering: Gathering {
self_gid: gid,
owner_pid: host.base.pid,
host_pid: host.base.pid,
..session.gathering.clone()
},
session_key: (0..32).map(|_| random()).collect(),
..session
};
return Self {
session: mm_session,
connected_players: Default::default(),
}
}
}
}
pub async fn add_players(
&mut self,
conns: &[WeakPassthroughInitModule<MatchmakeUser>],
join_msg: String,
) {
let Some(initiating_user) = conns[0].upgrade() else {
return;
};
let initiating_pid = initiating_user.base.pid;
let old_particip = self.connected_players.clone();
for conn in conns {
self.connected_players.push(conn.clone());
}
self.session.participation_count = self.connected_players.len() as u32;
for other_connection in &conns[1..] {
let Some(other_conn) = other_connection.upgrade() else {
continue;
};
let other_pid = other_conn.base.pid;
/*if other_pid == self.session.gathering.owner_pid &&
joining_pid == self.session.gathering.owner_pid{
continue;
}*/
other_conn
.remote
.process_notification_event(NotificationEvent {
pid_source: initiating_pid,
notif_type: 122_000,
param_1: self.session.gathering.self_gid as PID,
param_2: other_pid,
str_param: "".into(),
#[cfg(feature = "third-notif-param")]
param_3: 0,
})
.await;
}
let list_of_connected_pids: Vec<_> = self
.connected_players
.iter()
.filter_map(|p| p.upgrade())
.map(|p| p.base.pid)
.collect();
for other_connection in conns {
let Some(other_conn) = other_connection.upgrade() else {
continue;
};
// let other_pid = other_conn.pid;
/*if other_pid == self.session.gathering.owner_pid &&
joining_pid == self.session.gathering.owner_pid{
continue;
}*/
for pid in &list_of_connected_pids {
other_conn
.remote
.process_notification_event(NotificationEvent {
pid_source: initiating_pid,
notif_type: 3001,
param_1: self.session.gathering.self_gid as PID,
param_2: *pid,
str_param: join_msg.clone(),
#[cfg(feature = "third-notif-param")]
param_3: self.connected_players.len() as _,
})
.await;
}
}
for old_conns in &old_particip {
let Some(old_conns) = old_conns.upgrade() else {
continue;
};
/*if old_conns.pid != self.session.gathering.host_pid {
continue;
}*/
for new_conn_pid in conns
.iter()
.filter_map(WeakPassthroughInitModule::upgrade)
.map(|c| c.base.pid)
{
old_conns
.remote
.process_notification_event(NotificationEvent {
pid_source: initiating_pid,
notif_type: 3001,
param_1: self.session.gathering.self_gid as PID,
param_2: new_conn_pid,
str_param: join_msg.clone(),
#[cfg(feature = "third-notif-param")]
param_3: self.connected_players.len() as _,
})
.await;
}
}
}
pub fn has_min_active_players(&self) -> bool {
self.connected_players
.iter()
.filter(|v| v.upgrade().is_some())
.count()
>= self.session.gathering.minimum_participants as _
}
#[inline]
pub fn get_host(&self) -> Option<PassthroughInitModule<MatchmakeUser>> {
self.get_active_players()
.find(|v| v.base.pid == self.session.gathering.host_pid)
}
#[inline]
pub fn is_reachable(&self) -> bool {
self.get_active_players()
.any(|v| v.base.pid == self.session.gathering.host_pid)
&& (if self.session.gathering.flags & PERSISTENT_GATHERING != 0 {
if self.has_min_active_players() {
true
} else {
self.session.open_participation
}
} else {
self.has_min_active_players()
}) & self.has_min_active_players()
}
#[inline]
pub fn is_joinable(&self) -> bool {
#[cfg(not(feature = "splatoon"))]
let is_open = self.session.open_participation;
#[cfg(feature = "splatoon")]
let is_open = if self.session.gamemode == 12 {
true
} else {
self.session.open_participation
};
self.is_reachable() && is_open
}
pub fn matches_criteria(
&self,
search_criteria: &MatchmakeSessionSearchCriteria,
) -> Result<bool, ErrorCode> {
// todo: implement the rest of the search criteria
if search_criteria.vacant_only {
if (self.connected_players.len() as u16 + search_criteria.vacant_participants)
> self.session.gathering.maximum_participants
{
return Ok(false);
}
}
if search_criteria.exclude_locked {
if !self.session.open_participation {
return Ok(false);
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "v3-5-0")]{
if search_criteria.exclude_system_password_set {
if self.session.system_password_enabled {
return Ok(false);
}
}
if search_criteria.exclude_user_password_set {
if self.session.user_password_enabled {
return Ok(false);
}
}
}
}
if !check_bounds_str(
self.session.gathering.minimum_participants,
&search_criteria.minimum_participants,
)
.ok_or(ErrorCode::Core_InvalidArgument)?
{
return Ok(false);
}
if !check_bounds_str(
self.session.gathering.maximum_participants,
&search_criteria.maximum_participants,
)
.ok_or(ErrorCode::Core_InvalidArgument)?
{
return Ok(false);
}
let game_mode: u32 = search_criteria
.game_mode
.parse()
.map_err(|_| ErrorCode::Core_InvalidArgument)?;
if self.session.gamemode != game_mode {
return Ok(false);
}
let mm_sys_type: u32 = search_criteria
.matchmake_system_type
.parse()
.map_err(|_| ErrorCode::Core_InvalidArgument)?;
if self.session.matchmake_system_type != mm_sys_type {
return Ok(false);
}
#[cfg(feature = "splatoon")]
{
if !search_criteria.attribs.get(0).is_some_and(|s| {
self.session
.attributes
.get(0)
.is_some_and(|a| s.0.contains(a))
}) {
return Ok(false);
}
if !search_criteria.attribs.get(2).is_some_and(|s| {
self.session
.attributes
.get(2)
.is_some_and(|a| s.0.contains(a))
}) {
return Ok(false);
}
if !search_criteria.attribs.get(3).is_some_and(|s| {
self.session
.attributes
.get(3)
.is_some_and(|a| s.0.contains(a))
}) {
return Ok(false);
}
}
Ok(true)
}
pub async fn migrate_ownership(&mut self, initiator_pid: PID) -> Result<(), ErrorCode> {
let players: Vec<_> = self
.connected_players
.iter()
.filter_map(|p| p.upgrade())
.collect();
let Some(new_owner) = players
.iter()
.find(|p| p.base.pid != self.session.gathering.owner_pid)
else {
self.session.gathering.owner_pid = 0;
return Ok(());
};
self.session.gathering.owner_pid = new_owner.base.pid;
self.broadcast_notification(&NotificationEvent {
pid_source: initiator_pid,
notif_type: OWNERSHIP_CHANGED,
param_1: self.session.gathering.self_gid as PID,
param_2: new_owner.base.pid,
..Default::default()
})
.await;
Ok(())
}
pub async fn migrate_host(&mut self, initiator_pid: PID) -> Result<(), ErrorCode> {
// let players: Vec<_> = self.connected_players.iter().filter_map(|p| p.upgrade()).collect();
self.session.gathering.host_pid = self.session.gathering.owner_pid;
self.broadcast_notification(&NotificationEvent {
pid_source: initiator_pid,
notif_type: HOST_CHANGED,
param_1: self.session.gathering.self_gid as PID,
..Default::default()
})
.await;
Ok(())
}
pub async fn remove_player_from_session(
&mut self,
pid: PID,
message: &str,
) -> Result<(), ErrorCode> {
self.connected_players
.retain(|u| u.upgrade().is_some_and(|u| u.base.pid != pid));
self.session.participation_count =
(self.connected_players.len() & u32::MAX as usize) as u32;
if pid == self.session.gathering.owner_pid {
self.migrate_ownership(pid).await?;
}
if pid == self.session.gathering.host_pid {
self.migrate_host(pid).await?;
}
// todo: support DisconnectChangeOwner
// todo: finish the rest of this
for player in self.connected_players.iter().filter_map(|p| p.upgrade()) {
player
.remote
.process_notification_event(NotificationEvent {
notif_type: 3008,
pid_source: pid,
param_1: self.session.gathering.self_gid as PID,
param_2: pid,
str_param: message.to_owned(),
..Default::default()
})
.await;
}
Ok(())
}
}
impl RnexManager for MatchmakeManager {
type User = MatchmakeUser;
type InitData = ConnectionInitData;
async fn init_new_user(
this: PassthroughInitModule<Self>,
mod_holder: &rnex_server::ModuleHolder,
remote: &rnex_rmc::RmcConnection,
_: &Self::InitData,
user: WeakPassthroughInitModule<Self::User>,
) -> Self::User {
MatchmakeUser {
base: mod_holder
.get_ref_init_pt()
.expect("matchmaking module cannot work without the base module"),
matchmake_manager: this,
remote: RemoteMatchMakingClientProtocol::new(remote.clone()),
this: user,
}
}
async fn post_init(user: &Self::User) {
let mut users = user.matchmake_manager.users.write().await;
users.insert(user.base.cid, user.this.clone());
drop(users);
let mut users = user.matchmake_manager.users_by_pid.write().await;
users.insert(user.base.pid, user.this.clone());
drop(users);
}
}

View file

@ -0,0 +1,719 @@
use std::{env, sync::Arc};
use rnex_base::user::BaseUser;
use rnex_base_protos::ResultsRange;
use rnex_mm_protos::{
LocalMatchMakingProtocol, RemoteMatchMakingClientProtocol,
matchmake::{
AutoMatchmakeParam, CreateMatchmakeSessionParam, Gathering, JoinMatchmakeSessionParam,
Matchmake, MatchmakeSession, MatchmakeSessionSearchCriteria,
},
matchmake_ext::MatchmakeExt,
matchmake_extension::MatchmakeExtension,
nat_traversal::NatTraversal,
nat_traversal::RemoteNatTraversalConsole,
notifications::{
NotificationEvent, RemoteNotification,
notification_types::{END_GATHERING, REQUEST_JOIN_GATHERING},
},
};
use rnex_rmc::{any::Any, response::ErrorCode, rmc_struct};
use rnex_server::{PassthroughInitModule, WeakPassthroughInitModule};
use rnex_util::{
PID,
station_url::{StationUrl, UrlOptions},
};
use tokio::sync::Mutex;
use tracing::{error, info};
use crate::matchmake::{ExtendedMatchmakeSession, MatchmakeManager};
/*cfg_if! {
if #[cfg(feature = "datastore")] {
use rnex_core::rmc::protocols::datastore::{DataStore, RawDataStore, RawDataStoreInfo, RemoteDataStore};
define_rmc_proto!(
proto UserProtocol{
Secure,
MatchmakeExtension,
MatchmakeExt,
Matchmake,
NatTraversal,
Ranking,
Utility,
DataStore,
MessageDelivery
}
);
} else {
define_rmc_proto!(
proto UserProtocol{
Secure,
MatchmakeExtension,
MatchmakeExt,
Matchmake,
NatTraversal,
Utility,
Ranking,
MessageDelivery
}
);
}
}*/
/// Connection tickets are allowances to join a specific lobby, they are given out as soon as nat checks pass,
/// there are 2 stages of tickets because both sides have to do nat checking before we let the player join
/// the lobby
pub struct ConnectionTicket {
pub cid: u32,
pub result: bool,
}
#[derive(Debug)]
#[rmc_struct(MatchMakingProtocol)]
pub struct MatchmakeUser {
pub base: PassthroughInitModule<BaseUser>,
/*
pub pid: PID,
pub cid: u32,
pub ip: PRUDPSockAddr,
*/
pub this: WeakPassthroughInitModule<MatchmakeUser>,
pub remote: RemoteMatchMakingClientProtocol,
//pub station_url: RwLock<Vec<StationUrl>>,
pub matchmake_manager: PassthroughInitModule<MatchmakeManager>,
}
impl MatchmakeExtension for MatchmakeUser {
async fn close_participation(&self, gid: u32) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session.session.open_participation = false;
Ok(())
}
async fn open_participation(&self, gid: u32) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session.session.open_participation = true;
Ok(())
}
async fn browse_matchmake_session(
&self,
browse_criteria: MatchmakeSessionSearchCriteria,
result_range: ResultsRange,
) -> Result<Vec<Any<Gathering>>, ErrorCode> {
let results = self
.matchmake_manager
.search_by_criteria(&[browse_criteria])
.await?;
let mm_list = result_range.make_from_list(&results[..]);
let mut list = Vec::with_capacity(mm_list.len());
for mm_sess in mm_list {
let mm_sess = mm_sess.lock().await;
list.push(Any::new(&mm_sess.session).expect("type error"));
}
Ok(list)
}
async fn get_playing_session(&self, _pids: Vec<u32>) -> Result<Vec<()>, ErrorCode> {
Ok(Vec::new())
}
#[cfg(feature = "v3-5-0")]
async fn update_progress_score(&self, gid: u32, progress: u8) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session.session.progress_score = progress;
Ok(())
}
async fn create_matchmake_session_with_param(
&self,
create_session_param: CreateMatchmakeSessionParam,
) -> Result<MatchmakeSession, ErrorCode> {
println!("{:?}", create_session_param);
let gid = self.matchmake_manager.next_gid();
let mut new_session = ExtendedMatchmakeSession::from_matchmake_session(
gid,
create_session_param.matchmake_session,
&self.this.clone(),
)
.await;
let mut joining_players = vec![self.this.clone()];
let users = self.matchmake_manager.users.read().await;
if let Ok(old_gathering) = self
.matchmake_manager
.get_session(create_session_param.gid_for_participation_check)
.await
{
let old_gathering = old_gathering.lock().await;
let players = old_gathering
.connected_players
.iter()
.filter_map(|v| v.upgrade())
.filter(|u| {
create_session_param
.additional_participants
.iter()
.any(|p| *p == u.base.pid)
});
for player in players {
joining_players.push(PassthroughInitModule::downgrade(&player));
}
}
drop(users);
new_session.session.participation_count = create_session_param.participation_count as u32;
new_session
.add_players(&joining_players, create_session_param.join_message)
.await;
let session = new_session.session.clone();
let mut sessions = self.matchmake_manager.sessions.write().await;
sessions.insert(gid, Arc::new(Mutex::new(new_session)));
drop(sessions);
Ok(session)
}
async fn join_matchmake_session_with_param(
&self,
join_session_param: JoinMatchmakeSessionParam,
) -> Result<MatchmakeSession, ErrorCode> {
let session = self
.matchmake_manager
.get_session(join_session_param.gid)
.await?;
let mut session = session.lock().await;
#[cfg(feature = "v3-5-0")]
{
if join_session_param.user_password != session.session.user_password {
return Err(ErrorCode::RendezVous_MatchmakeSessionUserPasswordUnmatch);
}
}
session
.connected_players
.retain(|v| v.upgrade().is_some_and(|v| v.base.pid != self.base.pid));
let mut joining_players = vec![self.this.clone()];
let users = self.matchmake_manager.users.read().await;
if let Ok(old_gathering) = self
.matchmake_manager
.get_session(join_session_param.gid_for_participation_check)
.await
{
let old_gathering = old_gathering.lock().await;
let players = old_gathering
.connected_players
.iter()
.filter_map(|v| v.upgrade())
.filter(|u| {
join_session_param
.additional_participants
.iter()
.any(|p| *p == u.base.pid)
});
for player in players {
joining_players.push(PassthroughInitModule::downgrade(&player));
}
}
drop(users);
session
.add_players(&joining_players, join_session_param.join_message)
.await;
let mm_session = session.session.clone();
Ok(mm_session)
}
async fn auto_matchmake_with_param_postpone(
&self,
param: AutoMatchmakeParam,
) -> Result<MatchmakeSession, ErrorCode> {
println!("{:?}", param);
let mut joining_players = vec![self.this.clone()];
let users = self.matchmake_manager.users.read().await;
if let Ok(old_gathering) = self
.matchmake_manager
.get_session(param.gid_for_participation_check)
.await
{
let old_gathering = old_gathering.lock().await;
let players = old_gathering
.connected_players
.iter()
.filter_map(|v| v.upgrade())
.filter(|u| {
param
.additional_participants
.iter()
.any(|p| *p == u.base.pid)
});
for player in players {
joining_players.push(PassthroughInitModule::downgrade(&player));
}
}
drop(users);
let sessions = self
.matchmake_manager
.search_by_criteria(&param.search_criteria[..])
.await?;
if let Some(session) = sessions.get(0) {
let mut session = session.lock().await;
session
.add_players(&joining_players, param.join_message)
.await;
return Ok(session.session.clone());
}
drop(sessions);
println!("making new session!");
let AutoMatchmakeParam {
join_message,
participation_count,
gid_for_participation_check,
matchmake_session,
additional_participants,
..
} = param;
self.create_matchmake_session_with_param(CreateMatchmakeSessionParam {
join_message,
participation_count,
gid_for_participation_check,
create_matchmake_session_option: 0,
matchmake_session,
additional_participants,
})
.await
}
async fn find_matchmake_session_by_gathering_id_detail(
&self,
gid: u32,
) -> Result<MatchmakeSession, ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let session = session.lock().await;
Ok(session.session.clone())
}
async fn modify_current_game_attribute(
&self,
gid: u32,
attrib_index: u32,
attrib_val: u32,
) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session.session.attributes[attrib_index as usize] = attrib_val;
Ok(())
}
async fn create_matchmake_session(
&self,
gathering: Any<Gathering>,
message: String,
) -> Result<(u32, Vec<u8>), ErrorCode> {
info!("gathering: {:?}", gathering);
let session: MatchmakeSession = gathering.try_get_as()?;
let session = self
.create_matchmake_session_with_param(CreateMatchmakeSessionParam {
matchmake_session: session,
additional_participants: vec![],
gid_for_participation_check: 0,
create_matchmake_session_option: 0,
join_message: message,
participation_count: 1,
})
.await?;
Ok((session.gathering.self_gid, session.session_key))
}
async fn get_friend_notification_data(
&self,
_ty: i32,
) -> Result<Vec<NotificationEvent>, ErrorCode> {
Ok(vec![])
}
async fn update_notification_data(
&self,
ty: u32,
param_1: u32,
param_2: u32,
str_param: String,
) -> Result<(), ErrorCode> {
let recpipent = param_2;
let Some(user) = self
.matchmake_manager
.users_by_pid
.read()
.await
.get(&bytemuck::cast(recpipent))
.and_then(|v| v.upgrade())
else {
return Err(ErrorCode::Core_InvalidArgument);
};
println!("notif ty : {}", ty);
match ty {
REQUEST_JOIN_GATHERING => {
user.remote
.process_notification_event(NotificationEvent {
pid_source: self.base.pid,
notif_type: REQUEST_JOIN_GATHERING * 1000,
param_1: bytemuck::cast(param_1),
param_2: bytemuck::cast(param_2),
#[cfg(feature = "third-notif-param")]
param_3: 0,
str_param,
})
.await;
}
END_GATHERING => {
user.remote
.process_notification_event(NotificationEvent {
pid_source: self.base.pid,
notif_type: END_GATHERING * 1000,
param_1: bytemuck::cast(param_1),
param_2: bytemuck::cast(param_2),
#[cfg(feature = "third-notif-param")]
param_3: 0,
str_param,
})
.await;
}
_ => {
return Err(ErrorCode::Core_InvalidArgument);
}
}
Ok(())
}
async fn update_application_buffer(
&self,
gid: u32,
application_buffer: Vec<u8>,
) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
if session.session.gathering.host_pid == self.base.pid {
return Err(ErrorCode::RendezVous_PermissionDenied);
}
if session.session.gathering.owner_pid == self.base.pid {
return Err(ErrorCode::RendezVous_PermissionDenied);
}
session.session.application_buffer = application_buffer;
Ok(())
}
async fn join_matchmake_session_ex(
&self,
gid: u32,
message: String,
_dont_care_block_list: bool,
//participation_count: u16,
) -> Result<Vec<u8>, ErrorCode> {
let sess = self.matchmake_manager.get_session(gid).await?;
let mut sess = sess.lock().await;
sess.add_players(&[self.this.clone()], message).await;
Ok(sess.session.session_key.clone())
}
async fn auto_matchmake_with_search_criteria_postpone(
&self,
criteria: Vec<MatchmakeSessionSearchCriteria>,
gathering: Any<Gathering>,
join_message: String,
) -> Result<Any<Gathering>, ErrorCode> {
let session: MatchmakeSession = gathering.try_get_as()?;
println!("{:?}", criteria);
let session = self
.auto_matchmake_with_param_postpone(AutoMatchmakeParam {
matchmake_session: session,
additional_participants: vec![],
gid_for_participation_check: 0,
auto_matchmake_option: 0,
join_message,
participation_count: 0,
search_criteria: criteria,
target_gids: vec![],
})
.await?;
let any = Any::new(&session).map_err(|_| ErrorCode::Core_SystemError)?;
Ok(any)
}
}
impl Matchmake for MatchmakeUser {
async fn find_by_single_id(&self, gid: u32) -> Result<(bool, Any<Gathering>), ErrorCode> {
let s = self.matchmake_manager.get_session(gid).await?;
let s = s.lock().await;
Ok((
true,
Any::new(&s.session).map_err(|_| ErrorCode::Custom_Unknown)?,
))
}
async fn unregister_gathering(&self, _gid: u32) -> Result<bool, ErrorCode> {
Ok(true)
}
async fn get_session_urls(&self, gid: u32) -> Result<Vec<StationUrl>, ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let session = session.lock().await;
let urls: Vec<_> = session
.connected_players
.iter()
.filter_map(|v| v.upgrade())
.filter(|u| u.base.pid == session.session.gathering.host_pid)
.map(|u| async move { u.base.station_url.read().await.clone() })
.next()
.ok_or(ErrorCode::RendezVous_SessionClosed)?
.await;
println!("{:?}", urls);
if urls.is_empty() {
return Err(ErrorCode::RendezVous_NotParticipatedGathering);
}
Ok(urls)
}
async fn update_session_host(
&self,
gid: u32,
change_session_owner: bool,
) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session.session.gathering.host_pid = self.base.pid;
for player in &session.connected_players {
let Some(player) = player.upgrade() else {
continue;
};
player
.remote
.process_notification_event(NotificationEvent {
notif_type: 110_000,
pid_source: self.base.pid,
param_1: gid as PID,
param_2: self.base.pid,
#[cfg(feature = "third-notif-param")]
param_3: 0,
str_param: "".to_string(),
})
.await;
}
if change_session_owner {
session.session.gathering.owner_pid = self.base.pid;
for player in &session.connected_players {
let Some(player) = player.upgrade() else {
continue;
};
player
.remote
.process_notification_event(NotificationEvent {
notif_type: 4000,
pid_source: self.base.pid,
param_1: gid as PID,
param_2: self.base.pid,
#[cfg(feature = "third-notif-param")]
param_3: 0,
str_param: "".to_string(),
})
.await;
}
}
Ok(())
}
async fn migrate_gathering_ownership(
&self,
gid: u32,
candidates: Vec<PID>,
_participants_only: bool,
) -> Result<(), ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
let candidate = candidates.get(0).ok_or(ErrorCode::Core_InvalidArgument)?;
session.session.gathering.owner_pid = *candidate;
for player in &session.connected_players {
let Some(player) = player.upgrade() else {
continue;
};
player
.remote
.process_notification_event(NotificationEvent {
notif_type: 4000,
pid_source: self.base.pid,
param_1: gid as PID,
param_2: *candidate as PID,
#[cfg(feature = "third-notif-param")]
param_3: 0,
str_param: "".to_string(),
})
.await;
}
Ok(())
}
}
impl MatchmakeExt for MatchmakeUser {
async fn end_participation(&self, gid: u32, message: String) -> Result<bool, ErrorCode> {
let session = self.matchmake_manager.get_session(gid).await?;
let mut session = session.lock().await;
session
.remove_player_from_session(self.base.pid, &message)
.await?;
Ok(true)
}
}
impl NatTraversal for MatchmakeUser {
async fn report_nat_properties(
&self,
nat_mapping: u32,
nat_filtering: u32,
_rtt: u32,
) -> Result<(), ErrorCode> {
let mut urls = self.base.station_url.write().await;
for station_url in urls.iter_mut() {
station_url.options.retain(|o| match o {
UrlOptions::NatMapping(_) | UrlOptions::NatFiltering(_) => false,
_ => true,
});
station_url
.options
.push(UrlOptions::NatMapping(nat_mapping as u8));
station_url
.options
.push(UrlOptions::NatFiltering(nat_filtering as u8));
}
Ok(())
}
async fn report_nat_traversal_result(
&self,
cid: u32,
result: bool,
_rtt: u32,
) -> Result<(), ErrorCode> {
Ok(())
}
async fn request_probe_initiation(&self, _station_to_probe: String) -> Result<(), ErrorCode> {
info!("NO!");
Err(ErrorCode::RendezVous_AccountExpired)
}
async fn request_probe_initialization_ext(
&self,
target_list: Vec<StationUrl>,
station_to_probe: String,
) -> Result<(), ErrorCode> {
let users = self.matchmake_manager.users.read().await;
println!(
"requesting station probe for {:?} to {:?}",
target_list, station_to_probe
);
for url in target_list {
let Some(UrlOptions::RVConnectionID(v)) = url
.options
.into_iter()
.find(|o| matches!(o, &UrlOptions::RVConnectionID(_)))
else {
continue;
};
let Some(v) = users.get(&v) else {
continue;
};
let Some(user) = v.upgrade() else {
continue;
};
user.remote
.request_probe_initiation(station_to_probe.clone())
.await;
}
info!("finished probing");
Ok(())
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "rnex-msg"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = true

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,37 @@
impl MessageDelivery for User {
async fn deliver_message(&self, mut message: Any<UserMessage>) -> Result<(), ErrorCode> {
let mut msg = message.get()?;
let _users = match msg.recipient_type {
1 => {
let Some(user) = self
.matchmake_manager
.users_by_pid
.read()
.await
.get(&msg.recipient_id)
.map(Weak::upgrade)
.flatten()
else {
return Err(ErrorCode::Core_InvalidArgument);
};
if msg.flags & 1 != 0 {
msg.recipient_id = user.pid;
msg.recipient_type = 1;
}
message.emplace_parent(&msg)?;
user.remote.deliver_message(message).await;
}
2 => {
return Err(ErrorCode::Core_NotImplemented);
}
_ => {
return Err(ErrorCode::Core_InvalidArgument);
}
};
Err(ErrorCode::Core_NotImplemented)
}
}

View file

@ -0,0 +1,12 @@
[package]
name = "rnex-node-holder"
version = "0.1.0"
edition = "2024"
[dependencies]
rnex-rmc = { path = "../../rnex-rmc" }
rnex-reggie-protos = { path = "../../rnex-protocols/reggie-protos" }
rnex-server = { path = "../../rnex-server" }
tokio = { version = "1.52.3", features = ["sync"] }
[lints]
workspace = true

View file

@ -0,0 +1,84 @@
use std::{
convert::Infallible,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
sync::{Arc, Weak},
};
use rnex_reggie_protos::reggie::{
EdgeNodeHolderConnectOption, EdgeNodeManagement, LocalEdgeNodeHolder, RemoteEdgeNodeHolder,
};
use rnex_rmc::{response::ErrorCode, rmc_struct, tracing::info};
use rnex_server::{
ConnectionInitData, PassthroughInitModule, RnexManager, RnexModule, WeakPassthroughInitModule,
};
use tokio::sync::RwLock;
#[derive(Debug)]
#[rmc_struct(EdgeNodeHolder)]
pub struct EdgeNode {
em: PassthroughInitModule<NodeHolderManager>,
address: SocketAddrV4,
}
impl EdgeNodeManagement for EdgeNode {
async fn get_url(&self, seed: u64) -> Result<SocketAddrV4, ErrorCode> {
let nodes = self.em.edge_nodes.read().await;
let nodes: Vec<_> = nodes.iter().filter_map(|n| n.upgrade()).collect();
// avoid a devide by zero
if nodes.len() == 0 {
return Err(ErrorCode::Core_InvalidIndex);
};
let node = &nodes[seed as usize % nodes.len()];
Ok(node.address)
}
}
#[derive(Default, Debug)]
pub struct NodeHolderManager {
edge_nodes: RwLock<Vec<Weak<EdgeNode>>>,
}
#[derive(Default, Debug)]
pub struct NodeHolderModule;
impl RnexManager for NodeHolderManager {
type User = Arc<EdgeNode>;
type InitData = EdgeNodeHolderConnectOption;
async fn init_new_user(
this: PassthroughInitModule<Self>,
mod_holder: &rnex_server::ModuleHolder,
remote: &rnex_rmc::RmcConnection,
init_data: &Self::InitData,
_: WeakPassthroughInitModule<Self::User>,
) -> Self::User {
match init_data {
EdgeNodeHolderConnectOption::DontRegister => Arc::new(EdgeNode {
em: this,
address: SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0),
}),
EdgeNodeHolderConnectOption::Register(socket_addr_v4) => {
let node = Arc::new(EdgeNode {
em: this.clone(),
address: *socket_addr_v4,
});
this.edge_nodes.write().await.push(Arc::downgrade(&node));
node
}
}
}
}
impl RnexModule for NodeHolderModule {
type Manager = NodeHolderManager;
type InitError = Infallible;
async fn create_manager(
_: &rnex_server::ModuleHolder,
) -> Result<Self::Manager, Self::InitError> {
Ok(NodeHolderManager::default())
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "rnex-rk"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = true

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,155 @@
#[derive(Serialize, Deserialize)]
pub struct CompetitionPostResults {
pub splatfest_id: u32,
pub score: u32,
pub team_id: u8,
pub team_win: u8,
pub user: PID,
}
// Seperate function because I cannot give a fuck right now
async fn fetch_team_votes(fest_id: u32) -> Result<Vec<u32>, ErrorCode> {
let endpoint_votes = env::var("RNEX_SPLATOON_RESULTS_VOTES_GET").map_err(|_| {
error!("RNEX_SPLATOON_RESULTS_VOTES_GET not set");
ErrorCode::RendezVous_InvalidConfiguration
})?;
let url_votes = format!("{}?splatfest_id={}", endpoint_votes, fest_id);
let mut response = tokio::task::spawn_blocking(|| {
ureq::get(&url_votes).call().map_err(|e| {
error!("GET for votes failed: {:?}", e);
ErrorCode::RendezVous_InvalidConfiguration
})
})
.await?;
let body = response.body_mut().read_to_string().map_err(|e| {
error!("failed to read votes body: {:?}", e);
ErrorCode::RendezVous_InvalidConfiguration
})?;
let body = body.trim().trim_start_matches('[').trim_end_matches(']');
let votes: Result<Vec<u32>, _> = body.split(',').map(|s| u32::from_str(s.trim())).collect();
votes.map_err(|e| {
error!("failed to parse votes: {:?}", e);
ErrorCode::RendezVous_InvalidConfiguration
})
}
impl Ranking for User {
async fn competition_ranking_get_param(
&self,
param: CompetitionRankingGetParam,
) -> Result<Vec<CompetitionRankingScoreInfo>, ErrorCode> {
let fest_id = param.festival_ids.get(0).copied().unwrap_or(0);
let endpoint_results = env::var("RNEX_SPLATOON_RESULTS_GET").map_err(|_| {
error!("RNEX_SPLATOON_RESULTS_GET not set");
ErrorCode::RendezVous_InvalidConfiguration
})?;
let url_results = format!("{}?splatfest_id={}", endpoint_results, fest_id);
let response_results = ureq::get(&url_results).call();
let results: Vec<CompetitionPostResults> = match response_results {
Ok(mut res) => res.body_mut().read_json().map_err(|e| {
error!("failed to parse JSON: {:?}", e);
ErrorCode::RendezVous_InvalidConfiguration
})?,
Err(e) => {
error!("GET failed: {:?}", e);
return Err(ErrorCode::RendezVous_InvalidConfiguration);
}
};
let offset = param.range.offset as usize;
let size = param.range.size as usize;
let start = offset.min(results.len());
let end = (start + size).min(results.len());
let team_votes = fetch_team_votes(fest_id)?;
let mut wins = vec![0u32, 0u32];
for r in &results {
let won_team = (r.team_id ^ (!r.team_win)) & 1;
if let Some(team) = wins.get_mut(won_team as usize) {
*team += 1
};
}
let score_data: Vec<CompetitionRankingScoreData> = results[start..end]
.iter()
.map(|r| CompetitionRankingScoreData {
unk: 1,
pid: r.user,
score: r.score,
modified: KerberosDateTime::now(),
unk2: 1,
appdata: QBuffer(vec![]),
})
.collect();
let info = CompetitionRankingScoreInfo {
fest_id,
score_data,
unk: 0,
team_wins: wins,
team_votes,
};
println!("range: {:?}", param.range);
Ok(vec![info])
}
async fn upload_competition_ranking_score(
&self,
param: UploadCompetitionData,
) -> Result<bool, ErrorCode> {
info!("fest results for user {:?}:", self.pid);
info!("fest id: {:?}", param.splatfest_id);
info!("score: {:?}", param.score);
info!("team id: {:?}", param.team_id);
info!("did current team win: {:?}", param.team_win);
let endpoint = match env::var("RNEX_SPLATOON_RESULTS_POST") {
Ok(url) => url,
Err(_) => {
error!("RNEX_SPLATOON_RESULTS_POST not set");
return Ok(false);
}
};
let payload = CompetitionPostResults {
splatfest_id: param.splatfest_id,
score: param.score,
team_id: param.team_id,
team_win: param.team_win,
user: self.pid,
};
let json_body = match serde_json::to_string(&payload) {
Ok(j) => j,
Err(e) => {
error!("error making json_body: {:?}", e);
return Ok(false);
}
};
let response = ureq::post(&endpoint)
.header("Content-Type", "application/json")
.send(json_body);
match response {
Ok(res) => {
info!("POST worked: {}", res.status());
}
Err(e) => {
error!("POST borked: {:?}", e);
}
}
Ok(true)
}
}