use crate::error::{Error, Result};
use crate::ftp::FtpClient;
use crate::game_paths::{get_game_path, get_plugin_path, get_plugins_path};
use crate::ip_addr::{get_ip, verify_ip};
use crate::tcp_listen;
use crate::{build, cargo_info};
use owo_colors::OwoColorize;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use temp_git::TempGitDir;
mod temp_git;
fn connect(ip: IpAddr, print: bool) -> Result<FtpClient> {
if print {
println!("Connecting to ip '{}'...", ip);
}
let mut client = FtpClient::connect(ip)?;
client.login("anonymous", "anonymous")?;
if print {
println!("{}", "Connected!".green());
}
Ok(client)
}
fn warn_if_old_skyline_subsdk(client: &mut FtpClient, exefs_path: &str) {
let list = client.ls(Some(exefs_path)).unwrap();
let subsdk_count = list.matches("subsdk").count();
if subsdk_count > 1 {
println!(
"{}: An old install of skyline is detected, this may cause problems.",
"WARNING".yellow()
);
}
}
fn parse_tid(tid: &str) -> u64 {
u64::from_str_radix(tid, 16).expect("Invalid Title ID")
}
static SKYLINE_URL: &str =
"https://github.com/skyline-dev/skyline/releases/download/beta/skyline.zip";
static TEMPLATE_NPDM: &[u8] = include_bytes!("template.npdm");
pub fn generate_npdm(tid: &str) -> Vec<u8> {
[
&TEMPLATE_NPDM[..0x340],
&parse_tid(tid).to_le_bytes()[..],
&TEMPLATE_NPDM[0x348..],
]
.concat()
}
pub fn install(
ip: Option<String>,
title_id: Option<String>,
release: bool,
features: Vec<String>,
path: Option<String>,
no_default_features: bool,
) -> Result<()> {
let mut args = if release {
vec![String::from("--release")]
} else {
vec![]
};
if !features.is_empty() {
args.push(format!("--features={}", features.join(",")));
}
let (path, is_rom) = if let Some(path) = path.as_ref() {
if let Some(local_path) = path.strip_prefix("rom:/") {
Ok((local_path, true))
} else if let Some(absolute_path) = path.strip_prefix("sd:/") {
Ok((absolute_path, false))
} else {
Err(Error::BadSdPath)
}?
} else {
("skyline/plugins", true)
};
if no_default_features {
args.push("--no-default-features".to_owned());
}
let nro_path = build::build_get_nro(args)?;
let ip = verify_ip(get_ip(ip)?)?;
let mut client = connect(ip, true)?;
let metadata = cargo_info::get_metadata()?;
let title_id = title_id
.or_else(|| metadata.title_id.clone())
.ok_or(Error::NoTitleId)?;
println!("Ensuring directory exists...");
let _ = client.mkdir(&get_game_path(&title_id));
let _ = client.mkdir(&(get_game_path(&title_id) + "/exefs"));
let dirs = path
.split('/')
.filter(|x| !x.is_empty() && !x.ends_with(".nro"));
let mut plugin_folder_path = if is_rom {
format!("{}/romfs", get_game_path(&title_id))
} else {
String::from("")
};
let _ = client.mkdir(&plugin_folder_path);
for dir in dirs {
plugin_folder_path = format!("{}/{}", plugin_folder_path, dir);
let _ = client.mkdir(&plugin_folder_path);
}
warn_if_old_skyline_subsdk(&mut client, &(get_game_path(&title_id) + "/exefs/"));
let subsdk_path = get_game_path(&title_id) + "/exefs/subsdk9";
if !client.file_exists(&subsdk_path).unwrap_or(false) {
println!("Skyline subsdk not installed for the given title, downloading...");
let exefs = crate::package::get_exefs(SKYLINE_URL)?;
println!("Installing over subsdk9...");
client.put(&subsdk_path, exefs.subsdk1)?;
}
let npdm_path = get_game_path(&title_id) + "/exefs/main.npdm";
if !client.file_exists(&npdm_path).unwrap_or(false) {
println!("Skyline npdm not installed for the given title, generating and installing...");
client.put(&npdm_path, generate_npdm(&title_id))?;
}
for dep in &metadata.plugin_dependencies {
let dep_path = get_plugin_path(&title_id, &dep.name);
if !client.file_exists(&dep_path).unwrap_or(false) {
println!("Downloading dependency {}...", dep.name);
let dep_data = reqwest::blocking::get(&dep.url)
.map_err(|_| Error::DownloadError)?
.bytes()
.map_err(|_| Error::DownloadError)?;
println!("Installing dependency {}...", dep.name);
client.put(dep_path, &dep_data).unwrap();
}
}
let nro_name = if path.ends_with(".nro") {
path.split('/').last().unwrap()
} else {
nro_path
.file_name()
.and_then(|x| x.to_str())
.ok_or(Error::FailWriteNro)?
};
println!("Transferring file...");
client.put(
format!("{}/{}", plugin_folder_path, nro_name),
std::fs::read(nro_path)?,
)?;
Ok(())
}
pub fn from_git(
git: &str,
ip: Option<String>,
title_id: Option<String>,
release: bool,
features: Vec<String>,
path: Option<String>,
no_default_features: bool,
) -> Result<()> {
let temp_dir = TempGitDir::clone_to_current_dir(git)?;
install(ip, title_id, release, features, path, no_default_features)?;
temp_dir.delete();
Ok(())
}
use std::io::Write;
use std::net::TcpStream;
use std::time::Duration;
const RESTART_PLUGIN_PORT: u16 = 45423;
pub fn restart_game(ip: Option<String>, title_id: Option<String>) -> Result<()> {
let ip = verify_ip(get_ip(ip)?)?;
let mut port =
TcpStream::connect_timeout(&(ip, RESTART_PLUGIN_PORT).into(), Duration::from_secs(1))?;
let metadata = cargo_info::get_metadata()?;
let title_id = title_id
.or_else(|| metadata.title_id.clone())
.ok_or(Error::NoTitleId)?;
let title_id: u64 = u64::from_str_radix(&title_id, 0x10).unwrap_or(0);
port.write_all(&title_id.to_be_bytes())?;
Ok(())
}
pub fn install_and_run(
ip: Option<String>,
title_id: Option<String>,
release: bool,
restart: bool,
features: Vec<String>,
path: Option<String>,
no_default_features: bool,
) -> Result<()> {
install(
ip.clone(),
title_id.clone(),
release,
features,
path,
no_default_features,
)?;
if restart {
let restart_ip = ip.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
let _ = restart_game(restart_ip, title_id);
});
}
tcp_listen::listen(ip)
}
pub fn list(ip: Option<String>, title_id: Option<String>, path: Option<String>) -> Result<()> {
let ip = verify_ip(get_ip(ip)?)?;
let mut client = connect(ip, false)?;
if path.is_some() {
println!("{}", client.ls(Some(&path.unwrap()))?);
return Ok(());
}
let metadata = cargo_info::get_metadata()?;
let title_id = title_id.or(metadata.title_id).ok_or(Error::NoTitleId)?;
println!("{}", client.ls(Some(&get_plugins_path(&title_id)))?);
Ok(())
}
fn get_install_path(title_id: Option<String>, filename: Option<String>) -> Result<String> {
if filename.is_some() {
let filename_str = (&filename).as_ref().unwrap();
if filename_str.starts_with('/') {
return Ok(filename_str.to_string());
}
}
let metadata = cargo_info::get_metadata()?;
let filename = filename.unwrap_or(format!("lib{}.nro", metadata.name));
let title_id = title_id.or(metadata.title_id).ok_or(Error::NoTitleId)?;
Ok(get_plugin_path(&title_id, &filename))
}
pub fn rm(ip: Option<String>, title_id: Option<String>, filename: Option<String>) -> Result<()> {
let ip = verify_ip(get_ip(ip)?)?;
let mut client = connect(ip, false)?;
client.rm(get_install_path(title_id, filename)?)?;
Ok(())
}
pub fn cp(ip: Option<String>, title_id: Option<String>, src: String, dest: String) -> Result<()> {
let ip = verify_ip(get_ip(ip)?)?;
let mut client = connect(ip, false)?;
if dest.starts_with('/') {
return Err(Error::AbsSwitchPath);
}
let dest_path = PathBuf::from(&dest.replace("sd:/", "/"));
let mut install_path =
get_install_path(title_id, Some(dest_path.to_str().unwrap().to_string()))?;
let src_path = PathBuf::from(src);
let src_basename = src_path.file_name().unwrap();
let dest_basename = dest_path.file_name();
if dest_basename.is_none() || dest_basename.unwrap() != src_basename {
install_path = Path::new(&install_path)
.join(src_basename)
.to_str()
.unwrap()
.to_string();
}
println!("Transferring file to {}...", install_path);
client.put(install_path, std::fs::read(src_path.to_str().unwrap())?)?;
Ok(())
}