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

17
rnex-util/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "rnex-util"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.52.3", features = ["io-util", "macros", "rt", "sync"] }
tracing = "0.1.44"
chrono = "0.4.39"
bytemuck = { version = "1.25.0", features = ["derive"] }
md-5 = "0.11.0"
[features]
nx = []
[lints]
workspace = true

47
rnex-util/src/account.rs Normal file
View file

@ -0,0 +1,47 @@
use md5::{Digest, Md5};
use crate::PID;
#[derive(Clone, Debug)]
pub struct Account {
pub pid: PID,
pub username: String,
pub nex_key: [u8; 16],
}
impl Account {
pub fn new(pid: PID, username: &str, passwd: &str) -> Self {
let iteration_count = 65000 + pid % 1024;
// we do one iteration out here to ensure the key is always 16 bytes
let mut key: [u8; 16] = {
let mut md5 = Md5::new();
md5.update(passwd);
md5.finalize().into()
};
for _ in 1..iteration_count {
let mut md5 = Md5::new();
md5.update(key);
key = md5.finalize().into();
}
Self {
nex_key: key,
username: username.into(),
pid,
}
}
pub fn new_raw_key(pid: PID, username: &str, nex_key: [u8; 16]) -> Self {
Self {
username: username.into(),
pid,
nex_key,
}
}
pub fn get_login_data(&self) -> (PID, [u8; 16]) {
(self.pid, self.nex_key)
}
}

102
rnex-util/src/date_time.rs Normal file
View file

@ -0,0 +1,102 @@
use std::fmt::Display;
use bytemuck::{Pod, Zeroable};
use chrono::{Datelike, NaiveDate, Timelike, Utc};
use tracing::error;
#[derive(Pod, Zeroable, Copy, Clone, Debug, Eq, PartialEq, Default)]
#[repr(transparent)]
pub struct DateTime(pub u64);
impl Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}.{}.{} {}:{}:{}",
self.get_year(),
self.get_month(),
self.get_days(),
self.get_hours(),
self.get_minutes(),
self.get_seconds()
)
}
}
impl DateTime {
// this is the time which smm returned as the expriy date, we use it as a
// date so far into the future that it might as well just be never more generally
pub const PRACTICALLY_NEVER: Self = Self::new(0, 0, 0, 31, 12, 9999);
pub fn from_naive(dt: chrono::NaiveDateTime) -> Self {
use chrono::Datelike;
use chrono::Timelike;
Self::new(
dt.second() as u64,
dt.minute() as u64,
dt.hour() as u64,
dt.day() as u64,
dt.month() as u64,
dt.year() as u64,
)
}
pub const fn new(second: u64, minute: u64, hour: u64, day: u64, month: u64, year: u64) -> Self {
Self(second | (minute << 6) | (hour << 12) | (day << 17) | (month << 22) | (year << 26))
}
pub fn now() -> Self {
let now = chrono::Utc::now();
Self::new(
now.second() as u64,
now.minute() as u64,
now.hour() as u64,
now.day() as u64,
now.month() as u64,
now.year() as u64,
)
}
pub const fn get_seconds(&self) -> u8 {
(self.0 & 0b11_1111) as u8
}
pub const fn get_minutes(&self) -> u8 {
((self.0 >> 6) & 0b11_1111) as u8
}
pub const fn get_hours(&self) -> u8 {
((self.0 >> 12) & 0b1_1111) as u8
}
pub const fn get_days(&self) -> u8 {
((self.0 >> 17) & 0b11_1111) as u8
}
pub const fn get_month(&self) -> u8 {
((self.0 >> 22) & 0b1111) as u8
}
pub const fn get_year(&self) -> u64 {
(self.0 >> 26) & 0xFFFF_FFFF
}
pub fn to_regular_time(&self) -> chrono::DateTime<Utc> {
let date = match NaiveDate::from_ymd_opt(
self.get_year() as i32,
self.get_month() as u32,
self.get_days() as u32,
) {
Some(v) => v,
None => {
error!("invalid datetime...: {}", self);
Default::default()
}
};
chrono::NaiveDateTime::new(
date,
chrono::NaiveTime::from_hms_opt(
self.get_hours() as u32,
self.get_minutes() as u32,
self.get_seconds() as u32,
)
.unwrap_or_default(),
)
.and_utc()
}
}

182
rnex-util/src/lib.rs Normal file
View file

@ -0,0 +1,182 @@
#![allow(async_fn_in_trait)]
pub use tracing;
pub mod account;
pub mod date_time;
pub mod result;
pub mod station_url;
use std::ops::Deref;
use std::sync::{Arc, Weak};
use std::vec;
use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::sync::Notify;
use tokio::sync::mpsc::{Receiver, Sender, channel};
use tokio::task;
use tracing::{error, info};
#[cfg(feature = "nx")]
pub type PID = i64;
#[cfg(not(feature = "nx"))]
pub type PID = i32;
pub trait UnitPacketRead: AsyncRead + Unpin {
async fn read_buffer(&mut self) -> Result<Vec<u8>, io::Error> {
let mut len_raw: [u8; _] = [0; size_of::<usize>()];
self.read_exact(&mut len_raw).await?;
let len = usize::from_le_bytes(len_raw);
let mut vec = vec![0u8; len as _];
self.read_exact(&mut vec).await?;
Ok(vec)
}
}
impl<T: AsyncRead + Unpin> UnitPacketRead for T {}
pub trait UnitPacketWrite: AsyncWrite + Unpin {
async fn send_buffer(&mut self, data: &[u8]) -> Result<(), io::Error> {
let len_data = data.len().to_le_bytes();
self.write_all(&len_data[..]).await?;
self.write_all(data).await?;
self.flush().await?;
Ok(())
}
}
impl<T: AsyncWrite + Unpin> UnitPacketWrite for T {}
#[derive(Clone, Debug)]
pub struct SendingBufferConnection(Sender<Vec<u8>>, Arc<Notify>);
#[derive(Debug)]
pub struct SplittableBufferConnection(SendingBufferConnection, Receiver<Vec<u8>>);
impl AsRef<SendingBufferConnection> for SplittableBufferConnection {
fn as_ref(&self) -> &SendingBufferConnection {
&self.0
}
}
impl Deref for SplittableBufferConnection {
type Target = SendingBufferConnection;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl<T: Send + Unpin + AsyncWrite + AsyncRead + 'static> From<T> for SplittableBufferConnection {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl SplittableBufferConnection {
fn new<T: Send + Unpin + AsyncWrite + AsyncRead + 'static>(stream: T) -> Self {
let (outside_send, inside_recv) = channel::<Vec<u8>>(10);
let (inside_send, outside_recv) = channel::<Vec<u8>>(10);
let notify = Arc::new(Notify::new());
{
let notify = notify.clone();
task::spawn(async move {
let sender = inside_send;
let mut recver = inside_recv;
let mut stream = stream;
loop {
tokio::select! {
data = recver.recv() => {
let Some(data) = data else {
break;
};
if let Err(e) = stream.send_buffer(&data[..]).await{
error!("error sending data to backend: {e}");
break;
}
},
data = stream.read_buffer() => {
let data = match data{
Ok(d) => d,
Err(e) => {
error!("error reveiving data from backend: {e}");
break;
}
};
if let Err(e) = sender.send(data).await{
error!("a send error occurred {e}");
return;
}
},
() = notify.notified() => {
info!("shutting down connection");
break;
}
}
}
if let Err(e) = stream.shutdown().await {
error!("failed to shut down stream: {e}");
}
});
}
Self(SendingBufferConnection(outside_send, notify), outside_recv)
}
}
impl SendingBufferConnection {
pub async fn send(&self, buffer: Vec<u8>) -> Option<()> {
self.0.send(buffer).await.ok()
}
#[must_use]
pub fn is_alive(&self) -> bool {
!self.0.is_closed()
}
pub async fn disconnect(&self) {
while !self.0.is_closed() {
self.1.notify_waiters();
tokio::task::yield_now().await;
}
}
}
impl SplittableBufferConnection {
pub async fn recv(&mut self) -> Option<Vec<u8>> {
self.1.recv().await
}
#[must_use]
pub fn duplicate_sender(&self) -> SendingBufferConnection {
self.0.clone()
}
}
pub struct WeakVec<T>(Vec<Weak<T>>);
impl<T> WeakVec<T> {
pub fn new() -> Self {
Self(vec![])
}
pub fn from_vec(vec: Vec<Weak<T>>) -> Self {
Self(vec)
}
pub fn push(&mut self, val: Weak<T>) {
self.0.retain(|v| v.upgrade().is_some());
self.0.push(val);
}
pub fn iter(&self) -> impl Iterator<Item = Arc<T>> {
self.0.iter().filter_map(Weak::upgrade)
}
}

23
rnex-util/src/result.rs Normal file
View file

@ -0,0 +1,23 @@
use std::error::Error;
use tracing::error;
pub trait ResultExtension {
type Output;
fn display_err_or_some(self) -> Option<Self::Output>;
}
impl<T, U: Error> ResultExtension for Result<T, U> {
type Output = T;
fn display_err_or_some(self) -> Option<Self::Output> {
match self {
Ok(v) => Some(v),
Err(e) => {
error!("{e}");
None
}
}
}
}

View file

@ -0,0 +1,160 @@
use std::{
fmt::{Debug, Display, Formatter},
net::IpAddr,
};
use tracing::error;
use crate::PID;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Type {
UDP,
PRUDP,
PRUDPS,
}
pub mod nat_types {
pub const BEHIND_NAT: u8 = 1;
pub const PUBLIC: u8 = 2;
}
#[derive(Clone, Eq, PartialEq)]
pub enum UrlOptions {
Address(IpAddr),
Port(u16),
StreamType(u8),
StreamID(u8),
ConnectionID(u32),
ProbeInit(u32),
PrincipalID(PID),
NatType(u8),
NatMapping(u8),
NatFiltering(u8),
UPNP(u8),
RVConnectionID(u32),
Platform(u8),
PMP(u8),
PID(u32),
}
#[derive(Clone, PartialEq, Eq)]
pub struct StationUrl {
pub url_type: Type,
pub options: Vec<UrlOptions>,
}
impl StationUrl {
pub fn read_options(options: &str) -> Option<Vec<UrlOptions>> {
let mut options_out = Vec::new();
for option in options.split(';') {
if option == "" {
continue;
}
let mut option_parts = option.split('=');
let option_name = option_parts.next()?.to_ascii_lowercase();
let option_value = option_parts.next()?;
use UrlOptions::*;
match option_name.as_ref() {
"address" => options_out.push(Address(option_value.parse().ok()?)),
"port" => options_out.push(Port(option_value.parse().ok()?)),
"natf" => options_out.push(NatFiltering(option_value.parse().ok()?)),
"natm" => options_out.push(NatMapping(option_value.parse().ok()?)),
"sid" => options_out.push(StreamID(option_value.parse().ok()?)),
"upnp" => options_out.push(UPNP(option_value.parse().ok()?)),
"type" => options_out.push(NatType(option_value.parse().ok()?)),
"stream" => options_out.push(StreamType(option_value.parse().ok()?)),
"RVCID" => options_out.push(RVConnectionID(option_value.parse().ok()?)),
"rvcid" => options_out.push(RVConnectionID(option_value.parse().ok()?)),
"CID" => options_out.push(ConnectionID(option_value.parse().ok()?)),
"cid" => options_out.push(ConnectionID(option_value.parse().ok()?)),
"pl" => options_out.push(Platform(option_value.parse().ok()?)),
"pmp" => options_out.push(PMP(option_value.parse().ok()?)),
"pid" => options_out.push(PID(option_value.parse().ok()?)),
"PID" => options_out.push(PID(option_value.parse().ok()?)),
"probeinit" => options_out.push(ProbeInit(option_value.parse().ok()?)),
_ => {
error!("unimplemented option type, skipping: {}", option_name);
}
}
}
Some(options_out)
}
}
impl TryFrom<&str> for StationUrl {
type Error = ();
fn try_from(value: &str) -> Result<Self, ()> {
let (url_type, options) = value.split_at(value.find(":/").ok_or(())?);
let options = &options[2..];
use Type::*;
let url_type = match url_type {
"udp" => UDP,
"prudp" => PRUDP,
"prudps" => PRUDPS,
_ => return Err(()),
};
let options = Self::read_options(options).ok_or(())?;
Ok(Self { url_type, options })
}
}
impl Display for StationUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
use Type::*;
let url_type_str = match self.url_type {
UDP => "udp:/",
PRUDP => "prudp:/",
PRUDPS => "prudps:/",
};
write!(f, "{}", url_type_str)?;
use UrlOptions::*;
for option in &self.options {
match option {
Address(v) => write!(f, "address={}", v)?,
Port(v) => write!(f, "port={}", v)?,
StreamType(v) => write!(f, "stream={}", v)?,
StreamID(v) => write!(f, "sid={}", v)?,
ConnectionID(v) => write!(f, "CID={}", v)?,
PrincipalID(v) => write!(f, "PID={}", v)?,
NatType(v) => write!(f, "type={}", v)?,
NatMapping(v) => write!(f, "natm={}", v)?,
NatFiltering(v) => write!(f, "natf={}", v)?,
UPNP(v) => write!(f, "upnp={}", v)?,
RVConnectionID(v) => write!(f, "RVCID={}", v)?,
Platform(v) => write!(f, "pl={}", v)?,
PMP(v) => write!(f, "pmp={}", v)?,
PID(v) => write!(f, "PID={}", v)?,
ProbeInit(v) => write!(f, "probeinit={}", v)?,
}
write!(f, ";")?;
}
Ok(())
}
}
impl<'a> Into<String> for &'a StationUrl {
fn into(self) -> String {
let url = self.to_string();
url[0..url.len() - 1].into()
}
}
impl Debug for StationUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str: String = self.into();
f.write_str(&str)
}
}