1 Commits

Author SHA1 Message Date
fb7e76fe78 added cobble_core crate 2024-07-18 19:25:49 -06:00
158 changed files with 833 additions and 8514 deletions

8
.gitignore vendored
View File

@ -3,8 +3,6 @@
# will have compiled files and executables # will have compiled files and executables
debug/ debug/
target/ target/
logs/
**/logs/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@ -12,10 +10,4 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
**/*.log
FCLauncher/build/bin
FCLauncher/node_modules
FCLauncher/frontend/dist
FCLauncher/frontend/wailsjs/go/

View File

@ -1,11 +0,0 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"permissions": [
"updater:default"
]
}

View File

@ -1,20 +0,0 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-message",
"dialog:allow-ask",
"process:allow-restart",
"process:allow-exit",
"dialog:default",
"shell:default",
"process:default"
]
}

View File

@ -1,295 +0,0 @@
use std::fs::File;
use std::io::Cursor;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::modpack;
use crate::modpack::get_modpacks;
use crate::modpack::get_versions;
use crate::modpack::VersionEntry;
use crate::sftp;
use chrono;
use serde::Serialize;
use ssh2::Session;
use zip::write::SimpleFileOptions;
use zip::ZipArchive;
use zip::ZipWriter;
use tauri::Emitter;
//static USERNAME: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
//static PASSWORD: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
static SESSION: tauri::async_runtime::Mutex<Option<Session>> =
tauri::async_runtime::Mutex::const_new(None);
pub fn emit(event: &str, payload: impl Serialize + Clone, window: tauri::AppHandle) {
if !window.emit(event, payload).is_ok() {
println!("Failed to emit to window!");
}
}
#[tauri::command]
pub async fn login(username: String, password: String, window: tauri::AppHandle) {
let res = sftp::connect(username, password);
if res.is_ok() {
//*USERNAME.lock() = username;
//*PASSWORD.lock() = password;
*SESSION.lock().await = Some(res.unwrap());
emit("Login_Success", {}, window);
} else {
emit("Login_Failed", {}, window);
}
}
#[tauri::command]
pub async fn drop_session() {
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
session.disconnect(None, "disconnecting", None).unwrap();
}
*session = None;
}
async fn update_modpacks(modpacks: Vec<modpack::ModpackEntry>) -> Result<(), String> {
let data = serde_json::to_string(&modpacks).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(
None,
session.clone(),
PathBuf::from("/ftp/modpacks.json"),
reader,
format!("modpacks.json"),
data.as_bytes().len(),
)
.await?;
}
Err(format!("Session doesnt exist?"))
}
async fn update_versions(id: String, versions: Vec<modpack::VersionEntry>) -> Result<(), String> {
let data = serde_json::to_string(&versions).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(
None,
session.clone(),
PathBuf::from(format!("/ftp/{}/versions.json", id)),
reader,
format!("modpacks.json"),
data.as_bytes().len(),
)
.await?;
}
Err(format!("Session doesnt exist?"))
}
#[tauri::command]
pub async fn shift_up(id: String, window: tauri::AppHandle) {
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if pack.id == id {
break;
}
index += 1;
}
if index != 0 {
modpacks.swap(index, index - 1);
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn shift_down(id: String, window: tauri::AppHandle) {
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if pack.id == id {
break;
}
index += 1;
}
if index != modpacks.len() - 1 {
modpacks.swap(index, index + 1);
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn add_pack(id: String, name: String, window: tauri::AppHandle) {
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
let res = sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
let res = sftp::mkdir(
session.clone(),
PathBuf::from(format!("/ftp/{}/Versions", id)),
)
.await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
}
}
let versions: Vec<VersionEntry> = Vec::new();
let res = update_versions(id.clone(), versions).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
let mut modpacks = get_modpacks().await;
modpacks.push(modpack::ModpackEntry {
id: id,
name: name,
last_updated: format!("{:?}", chrono::offset::Utc::now()),
});
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn remove_pack(id: String, window: tauri::AppHandle) {
let mut modpacks = get_modpacks().await;
let mut index = 0;
for pack in modpacks.clone() {
if pack.id == id {
modpacks.remove(index);
break;
}
index += 1;
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::rmdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
}
}
}
#[tauri::command]
pub async fn update_pack(
window: tauri::AppHandle,
id: String,
path: String,
version: String,
) -> Result<(), String> {
println!(
"Update modpack {}, to version {}, from file {}",
id, version, path
);
let file = File::open(Path::new(path.as_str())).or(Err(format!("Unable to open file")))?;
let mut archive = ZipArchive::new(file).or(Err(format!("File not a zip archive!")))?;
let mut buf = Cursor::new(vec![]);
let mut out_archive = ZipWriter::new(&mut buf);
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.or(Err(format!("error reading archive")))?;
if file.name() == "overrides/version.txt" {
continue;
}
let res = out_archive.start_file(
file.name(),
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
);
if !res.is_ok() {
emit("Error", format!("Unable to start zip archive"), window);
return Err(format!("Unable to start zip archive"));
}
let res = std::io::copy(&mut file, &mut out_archive);
if !res.is_ok() {
emit("Error", format!("Unable to copy archive to ram"), window);
return Err(format!("Unable to copy archive to ram"));
}
}
let res = out_archive.start_file(
"overrides/version.txt",
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
);
if !res.is_ok() {
emit("Error", format!("Unable to create version file"), window);
return Err(format!("Unable to create version file"));
}
let res = out_archive.write_all(version.as_bytes());
if !res.is_ok() {
emit("Error", format!("Unable to write to zip"), window);
return Err(format!("Unable to write to zip"));
}
let res = out_archive.finish();
if !res.is_ok() {
emit("Error", format!("Unable to finish zip"), window);
return Err(format!("Unable to finish zip"));
}
buf.rewind().unwrap();
let timestamp = format!("{:?}", chrono::offset::Utc::now());
let path = format!("Versions/{}-{}.mrpack", id, timestamp);
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
let size = buf.clone().bytes().count();
let upload_path = format!("/ftp/{}/{}", id, path.clone());
println!("Uploading to {}", upload_path);
let res = sftp::uplaod(
Some(window.clone()),
session.clone(),
PathBuf::from(upload_path),
&mut buf,
path.clone(),
size,
)
.await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
}
}
let mut versions = get_versions(id.clone()).await?;
versions.push(VersionEntry {
Version: version,
Date: timestamp.clone(),
File: path,
});
let res = update_versions(id.clone(), versions).await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
let mut modpacks = get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if pack.id == id {
modpacks[index].last_updated = timestamp;
break;
}
index += 1;
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
Ok(())
}

View File

@ -1,67 +0,0 @@
use futures_util::StreamExt;
use serde::Serialize;
use std::cmp::min;
use std::io::Write;
use std::path::PathBuf;
use tauri::Emitter;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
pub downloaded: usize,
pub total: usize,
pub time_elapsed: usize,
pub download_name: String,
}
pub async fn download(
window: Option<tauri::AppHandle>,
url: String,
mut writer: impl Write,
downloadName: String,
) -> Result<(), String> {
let client = reqwest::Client::new();
let res = client
.get(url)
.send()
.await
.or(Err(format!("Failed to fetch from URL!")))?;
let total_size = res
.content_length()
.ok_or(format!("Failed to get content length"))?;
let mut downloaded: u64 = 0;
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item.or(Err(format!("Error while downloading file!")))?;
writer
.write_all(&chunk)
.or(Err("Error writing to stream!"))?;
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
println!(
"Downloading {}: {}MB / {}MB",
downloadName.clone(),
downloaded / (1024 * 1024),
total_size / (1024 * 1024)
);
if let Some(window) = window.clone() {
if downloaded != total_size {
window
.emit(
"download_progress",
DownloadStatus {
downloaded: downloaded as usize,
total: total_size as usize,
time_elapsed: 0,
download_name: downloadName.clone(),
},
)
.or(Err(format!("Unable to signal window")))?;
} else {
window
.emit("download_finished", true)
.or(Err(format!("Unable to signal window!")))?;
}
}
}
return Ok(());
}

View File

@ -1,110 +0,0 @@
use flate2::read::GzDecoder;
use std::env;
use std::io::{Cursor, Read, Seek};
use std::path::{Components, Path, PathBuf};
use tar::Archive;
use crate::https;
use crate::system_dirs::get_local_data_directory;
use crate::util;
fn check_java(version: u8) -> bool {
let dir = get_local_data_directory().join("java").join(format!(
"java-{}-{}",
version,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
}
));
dir.exists()
}
pub async fn install_java(version: u8, window: tauri::AppHandle) {
if check_java(version) {
return;
}
//let ftp_dir = PathBuf::new().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}));
let mut buff = Cursor::new(vec![]);
//ftp::ftp_retr(Some(window), ftp_dir, &mut buff, |window, data, size| util::download_callback(format!("Java {}", version), window,data, size)).unwrap();
https::download(
Some(window.clone()),
format!(
"https://gitea.piwalker.net/fclauncher/java/java-{}-{}",
version,
if env::consts::OS == "windows" {
"win.zip"
} else {
"lin.tar.gz"
}
),
&mut buff,
format!("Java {}", version),
)
.await;
std::fs::create_dir_all(get_local_data_directory().join("java").join(format!(
"java-{}-{}",
version,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
}
)))
.unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !unpack_archive(
archive,
get_local_data_directory()
.join("java")
.join(format!("java-{}-lin", version)),
)
.is_ok()
{
std::fs::remove_dir_all(
get_local_data_directory()
.join("java")
.join(format!("java-{}-lin", version)),
)
.unwrap();
}
} else {
if !zip_extract::extract(
buff,
get_local_data_directory()
.join("java")
.join(format!("java-{}-win", version))
.as_path(),
true,
)
.is_ok()
{
std::fs::remove_dir_all(
get_local_data_directory()
.join("java")
.join(format!("java-{}-win", version)),
)
.unwrap();
}
}
}
fn unpack_archive<T: Read>(
mut archive: Archive<T>,
dst: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
for file in archive.entries()? {
let path = PathBuf::new().join(dst.clone());
let mut file = file?;
let file_path = file.path()?;
let mut file_path = file_path.components();
let _ = file_path.next();
let file_path = file_path.as_path();
file.unpack(path.join(file_path))?;
}
Ok(())
}

View File

@ -1,124 +0,0 @@
use flate2::read::GzDecoder;
use std::{
env,
fs::File,
io::{BufRead, Cursor, Seek, Write},
path::PathBuf,
process::Command,
str::FromStr,
};
use tar::Archive;
//use tauri::file;
use crate::{
https, java,
system_dirs::{get_local_data_directory, get_prism_executable},
util,
};
pub fn check_prism() -> bool {
let path = get_local_data_directory().join("prism");
path.exists()
}
#[tauri::command]
pub async fn install_prism(window: tauri::AppHandle) {
if check_prism() {
return;
}
java::install_java(21, window.clone()).await;
let mut buff = Cursor::new(vec![]);
let mut total = 0;
//ftp::ftp_retr(Some(window.clone()), path, &mut buff, |window: Option<tauri::Window>, data, size| util::download_callback("Prism Launcher".to_string(),window, data, size)).unwrap();
https::download(
Some(window.clone()),
format!(
"https://gitea.piwalker.net/fclauncher/prism/prism-{}",
if env::consts::OS == "windows" {
"win.zip"
} else {
"lin.tar.gz"
}
),
&mut buff,
format!("Prism Launcher"),
)
.await;
std::fs::create_dir_all(get_local_data_directory().join("prism")).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !archive
.unpack(get_local_data_directory().join("prism"))
.is_ok()
{
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
} else {
if !zip_extract::extract(
buff,
get_local_data_directory().join("prism").as_path(),
true,
)
.is_ok()
{
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
}
let mut buff = Cursor::new(vec![]);
//ftp::ftp_retr(Some(window.clone()), PathBuf::new().join("prism").join("prismlauncher.cfg"), &mut buff, |_, _, _| return).unwrap();
https::download(
None,
format!("https://gitea.piwalker.net/fclauncher/prism/prismlauncher.cfg"),
&mut buff,
format!("prismlauncher.cfg"),
)
.await;
buff.rewind();
let mut file = File::create(
get_local_data_directory()
.join("prism")
.join("prismlauncher.cfg"),
)
.unwrap();
loop {
let mut buf = String::new();
let count = buff.read_line(&mut buf).unwrap();
if count == 0 {
break;
}
if buf.starts_with("JavaPath") {
buf = format!(
"JavaPath={}/java/java-21-{}\n",
get_local_data_directory()
.to_str()
.unwrap()
.replace("\\", "/"),
if env::consts::OS == "windows" {
"win"
} else {
"lin"
}
);
} else if buf.starts_with("LastHostname") {
buf = format!(
"LastHostname={}\n",
gethostname::gethostname().to_str().unwrap()
);
}
file.write_all(buf.as_bytes());
}
}
#[tauri::command]
pub fn launch_prism() {
let mut child = Command::new(
get_local_data_directory()
.join("prism")
.join(get_prism_executable()),
)
.spawn()
.unwrap();
}

View File

@ -1,102 +0,0 @@
use futures_util::future::BoxFuture;
use futures_util::io::BufReader;
use futures_util::io::Cursor;
use futures_util::FutureExt;
use ssh2::OpenFlags;
use ssh2::Session;
use ssh2::Sftp;
use std::io::prelude::*;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use tauri::Emitter;
use crate::https;
pub fn connect(username: String, password: String) -> Result<Session, String> {
let tcp = TcpStream::connect("gitea.piwalker.net:22")
.or(Err(format!("Unable to connect to host")))?;
let mut sess = Session::new().or(Err(format!("Unable to creat stream")))?;
sess.set_tcp_stream(tcp);
sess.handshake().unwrap();
sess.userauth_password(username.as_str(), password.as_str())
.or(Err(format!("Invalid username or password")))?;
Ok(sess)
}
pub async fn uplaod(
window: Option<tauri::AppHandle>,
sess: Session,
path: PathBuf,
mut reader: impl Read,
upload_name: String,
total_size: usize,
) -> Result<(), String> {
let sftp = sess.sftp().or(Err("unable to open sftp session"))?;
let mut file = sftp.create(path.as_path()).or(Err("Unable to open file"))?;
let mut uploaded = 0;
while true {
let mut buf = vec![0u8; 1024 * 32];
let res = reader.read(&mut buf).unwrap();
if res <= 0 {
if let Some(window) = window.clone() {
window
.emit("download_finished", true)
.or(Err(format!("Unable to signal window!")))?;
}
break;
}
file.write_all(buf.split_at(res).0).unwrap();
uploaded += res;
println!(
"Uploading {} {}MB / {}MB",
upload_name,
uploaded / (1024 * 1024),
total_size / (1024 * 1024)
);
if let Some(window) = window.clone() {
window
.emit(
"download_progress",
https::DownloadStatus {
downloaded: uploaded as usize,
total: total_size as usize,
time_elapsed: 0,
download_name: upload_name.clone(),
},
)
.or(Err(format!("Unable to signal window")))?;
}
}
Ok(())
}
pub async fn mkdir(sess: Session, path: PathBuf) -> Result<(), String> {
let sftp = sess.sftp().or(Err("Unable to open sftp session"))?;
sftp.mkdir(path.as_path(), 0o775)
.or(Err(format!("Unable to create directory")))?;
Ok(())
}
pub fn rmdir(sess: Session, path: PathBuf) -> BoxFuture<'static, ()> {
async move {
let sftp = sess.sftp().or(Err("Unable to open sftp session")).unwrap();
let dirs = sftp
.readdir(path.as_path())
.or(Err("unable to stat directory"))
.unwrap();
for dir in dirs {
if dir.1.is_dir() {
rmdir(sess.clone(), dir.0).await;
} else {
sftp.unlink(dir.0.as_path())
.or(Err(format!("Unable to delete file")))
.unwrap();
}
}
sftp.rmdir(path.as_path())
.or(Err(format!("Unable to delete directory")))
.unwrap();
}
.boxed()
}

View File

@ -1,35 +0,0 @@
use dirs::home_dir;
use std::{
env,
path::{Path, PathBuf},
};
pub fn get_local_data_directory() -> PathBuf {
dirs::data_local_dir().unwrap().join("FCLauncher")
}
pub fn get_data_directory() -> PathBuf {
dirs::data_dir().unwrap().join("FCLauncher")
}
pub fn get_java_executable() -> String {
return format!(
"java{}",
if env::consts::OS == "windows" {
".exe"
} else {
""
}
);
}
pub fn get_prism_executable() -> String {
return format!(
"{}",
if env::consts::OS == "windows" {
"prismlauncher.exe"
} else {
"PrismLauncher"
}
);
}

View File

@ -1,46 +0,0 @@
use std::clone;
use tauri::Emitter;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
downloaded: usize,
total: usize,
time_elapsed: usize,
download_name: String,
}
pub fn download_callback(
download_name: String,
window: Option<tauri::AppHandle>,
count: usize,
size: usize,
) {
unsafe {
static mut total: usize = 0;
total += count;
if let Some(window1) = window.clone() {
window1.emit(
"download_progress",
DownloadStatus {
downloaded: total,
total: size,
time_elapsed: 0,
download_name: download_name,
},
);
}
println!(
"Downloading {}MB / {}MB",
total / (1024 * 1024),
size / (1024 * 1024)
);
if count == 0 {
if let Some(window2) = window {
window2.emit("download_finished", true);
}
total = 0;
}
}
}

View File

@ -1,43 +0,0 @@
{
"build": {
"frontendDist": "../src"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"createUpdaterArtifacts": "v1Compatible"
},
"productName": "FCLauncher",
"mainBinaryName": "FCLauncher",
"version": "1.0.5",
"identifier": "net.piwalker",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDOEUwMjYxRUU2NEI5RgpSV1NmUytZZUp1RElBN3dEaGhpWG9JZVNQcFlnNFFzaXN0UnBsVmxNeVdWWnJoQmh4cGJRbjN3Ygo=",
"endpoints": [
"https://gitea.piwalker.net/fclauncher/app.json"
]
}
},
"app": {
"security": {
"csp": null
},
"withGlobalTauri": true,
"windows": [
{
"title": "FCLauncher",
"width": 800,
"height": 600,
"resizable": false
}
]
}
}

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -8,10 +8,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "1", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "1", features = [ "process-all", "dialog-open", "updater", "dialog-ask", "shell-open"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
suppaftp = { version = "6.0.1", features = ["native-tls"] } suppaftp = { version = "6.0.1", features = ["native-tls"] }
@ -27,13 +27,8 @@ futures-util = "0.3.30"
ssh2 = "0.9.4" ssh2 = "0.9.4"
chrono = "0.4.38" chrono = "0.4.38"
zip = "2.1.3" zip = "2.1.3"
tauri-plugin-dialog = "2" cobble-core = { version = "1.2.0", features = ["auth", "serde", "modded"] }
tauri-plugin-shell = "2"
tauri-plugin-process = "2"
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"

View File

@ -1,4 +1,4 @@
export TAURI_PRIVATE_KEY=$(cat ~/.tauri/fclauncher.key) export TAURI_PRIVATE_KEY=$(cat ~/.tauri/fclauncher.key)
read -s PASSWORD read -s PASSWORD
export TAURI_KEY_PASSWORD=$PASSWORD export TAURI_KEY_PASSWORD=$PASSWORD
NO_STRIP=true cargo tauri build cargo tauri build

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,185 @@
use std::io::Cursor;
use std::io::Read;
use std::fs::File;
use std::io::Seek;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::modpack::get_modpacks;
use crate::modpack::get_versions;
use crate::modpack::VersionEntry;
use crate::sftp;
use crate::modpack;
use crate::ModpackEntry;
use ssh2::Sftp;
use ssh2::Session;
use chrono;
use zip::unstable::write::FileOptionsExt;
use zip::write::FileOptions;
use zip::write::SimpleFileOptions;
use zip::ZipArchive;
use zip::ZipWriter;
//static USERNAME: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
//static PASSWORD: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
static SESSION: tauri::async_runtime::Mutex<Option<Session>> = tauri::async_runtime::Mutex::const_new(None);
#[tauri::command]
pub async fn login(username: String, password: String, window: tauri::Window) {
let res = sftp::connect(username, password);
if(res.is_ok()){
//*USERNAME.lock() = username;
//*PASSWORD.lock() = password;
*SESSION.lock().await = Some(res.unwrap());
window.emit("Login_Success", {});
}else{
window.emit("Login_Failed", {});
}
}
#[tauri::command]
pub async fn drop_session(){
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
session.disconnect(None, "disconnecting", None);
}
*session = None;
}
async fn update_modpacks(modpacks: Vec<modpack::ModpackEntry>) -> Result<(), String>{
let data = serde_json::to_string(&modpacks).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(None, session.clone(), PathBuf::from("/ftp/modpacks.json"), reader, format!("modpacks.json"), data.as_bytes().len()).await;
}
Ok(())
}
async fn update_versions(id: String, versions: Vec<modpack::VersionEntry>) -> Result<(), String>{
let data = serde_json::to_string(&versions).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(None, session.clone(), PathBuf::from(format!("/ftp/{}/versions.json", id)), reader, format!("modpacks.json"), data.as_bytes().len()).await;
}
Ok(())
}
#[tauri::command]
pub async fn shift_up(id: String){
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if(pack.id == id){
break;
}
index += 1;
}
if index != 0 {
modpacks.swap(index, index-1);
}
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn shift_down(id: String){
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if(pack.id == id){
break;
}
index += 1;
}
if index != modpacks.len()-1 {
modpacks.swap(index, index+1);
}
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn add_pack(id: String, name: String){
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}/Versions", id))).await;
}
}
let versions: Vec<VersionEntry> = Vec::new();
update_versions(id.clone(), versions).await;
let mut modpacks = get_modpacks().await;
modpacks.push(modpack::ModpackEntry{id: id, name: name, last_updated: format!("{:?}", chrono::offset::Utc::now())});
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn remove_pack(id: String){
let mut modpacks = get_modpacks().await;
let mut index = 0;
for pack in modpacks.clone() {
if pack.id == id {
modpacks.remove(index);
break;
}
index += 1;
}
update_modpacks(modpacks).await;
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
sftp::rmdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
}
}
}
#[tauri::command]
pub async fn update_pack(window: tauri::Window, id: String, path: String, version: String) -> Result<(), String>{
println!("Update modpack {}, to version {}, from file {}", id, version, path);
let file = File::open(Path::new(path.as_str())).or(Err(format!("Unable to open file")))?;
let mut archive = ZipArchive::new(file).or(Err(format!("File not a zip archive!")))?;
let mut buf = Cursor::new(vec![]);
let mut out_archive = ZipWriter::new(&mut buf);
for i in 0..archive.len() {
let mut file = archive.by_index(i).or(Err(format!("error reading archive")))?;
if(file.name() == "overrides/version.txt"){
continue;
}
out_archive.start_file(file.name(), SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated));
std::io::copy(&mut file, &mut out_archive);
}
out_archive.start_file("overrides/version.txt", SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated));
out_archive.write_all(version.as_bytes());
out_archive.finish();
buf.rewind().unwrap();
let timestamp = format!("{:?}", chrono::offset::Utc::now());
let path = format!("Versions/{}-{}.mrpack", id, timestamp);
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
let size = buf.clone().bytes().count();
let upload_path = format!("/ftp/{}/{}", id, path.clone());
println!("Uploading to {}", upload_path);
sftp::uplaod(Some(window), session.clone(), PathBuf::from(upload_path), &mut buf, path.clone(), size).await;
}
}
let mut versions = get_versions(id.clone()).await?;
versions.push(VersionEntry{Version: version, Date: timestamp.clone(), File: path});
update_versions(id.clone(), versions).await;
let mut modpacks = get_modpacks().await;
let mut index = 0;
for mut pack in modpacks.as_slice(){
if pack.id == id {
modpacks[index].last_updated = timestamp;
break;
}
index += 1;
}
update_modpacks(modpacks).await;
Ok(())
}

View File

@ -0,0 +1,39 @@
use std::io::Write;
use std::path::PathBuf;
use futures_util::StreamExt;
use std::cmp::min;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
pub downloaded: usize,
pub total: usize,
pub time_elapsed: usize,
pub download_name: String
}
pub async fn download(window: Option<tauri::Window>, url: String , mut writer: impl Write, downloadName: String) -> Result<(), String> {
let client = reqwest::Client::new();
let res = client.get(url).send().await.or(Err(format!("Failed to fetch from URL!")))?;
let total_size = res.content_length().ok_or(format!("Failed to get content length"))?;
let mut downloaded: u64 = 0;
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item.or(Err(format!("Error while downloading file!")))?;
writer.write_all(&chunk).or(Err("Error writing to stream!"))?;
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
println!("Downloading {}: {}MB / {}MB", downloadName.clone(), downloaded/(1024*1024), total_size/(1024*1024));
if let Some(window) = window.clone() {
if downloaded != total_size {
window.emit("download_progress", DownloadStatus{downloaded: downloaded as usize, total: total_size as usize, time_elapsed: 0, download_name: downloadName.clone()}).or(Err(format!("Unable to signal window")))?;
} else {
window.emit("download_finished", true).or(Err(format!("Unable to signal window!")))?;
}
}
}
return Ok(());
}

View File

@ -0,0 +1,52 @@
use std::env;
use std::io::{Cursor, Seek, Read};
use std::path::{Components, Path, PathBuf};
use flate2::read::GzDecoder;
use tar::Archive;
use crate::system_dirs::get_local_data_directory;
use crate::https;
use crate::util;
fn check_java(version: u8) -> bool {
let dir = get_local_data_directory().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win" } else {"lin"}));
dir.exists()
}
pub async fn install_java(version: u8, window: tauri::Window) {
if check_java(version) {
return;
}
//let ftp_dir = PathBuf::new().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}));
let mut buff = Cursor::new(vec![]);
//ftp::ftp_retr(Some(window), ftp_dir, &mut buff, |window, data, size| util::download_callback(format!("Java {}", version), window,data, size)).unwrap();
https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/java/java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}), &mut buff, format!("Java {}", version)).await;
std::fs::create_dir_all(get_local_data_directory().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win" } else {"lin"}))).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !unpack_archive(archive, get_local_data_directory().join("java").join(format!("java-{}-lin", version))).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("java").join(format!("java-{}-lin", version))).unwrap();
}
} else {
if !zip_extract::extract(buff, get_local_data_directory().join("java").join(format!("java-{}-win", version)).as_path(), true).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("java").join(format!("java-{}-win", version))).unwrap();
}
}
}
fn unpack_archive<T: Read>(mut archive: Archive<T>, dst: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
for file in archive.entries()? {
let path = PathBuf::new().join(dst.clone());
let mut file = file?;
let file_path = file.path()?;
let mut file_path = file_path.components();
let _ = file_path.next();
let file_path = file_path.as_path();
file.unpack(path.join(file_path))?;
}
Ok(())
}

View File

@ -1,28 +1,28 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use self_update::cargo_crate_version;
use serde::Deserialize;
use serde::Serialize;
use serde_json::{Map, Result, Value};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::io::Seek;
use std::{io::Cursor, path::PathBuf}; use std::{io::Cursor, path::PathBuf};
use std::io::Seek;
use self_update::cargo_crate_version;
use serde_json::{Map, Result, Value};
use serde::Serialize;
use serde::Deserialize;
//mod ftp; //mod ftp;
mod admin;
mod https;
mod java; mod java;
mod modpack;
mod prism; mod prism;
mod sftp;
mod system_dirs; mod system_dirs;
mod util; mod util;
mod modpack;
mod admin;
mod https;
mod sftp;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct ModpackEntry { struct ModpackEntry{
name: String, name: String,
id: String, id: String
} }
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
@ -31,30 +31,12 @@ fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name) format!("Hello, {}! You've been greeted from Rust!", name)
} }
fn main() { fn main() {
//modpack::get_modpacks(); //modpack::get_modpacks();
//prism::install_prism(); //prism::install_prism();
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build()) .invoke_handler(tauri::generate_handler![greet, modpack::get_modpacks, modpack::launch_modpack, modpack::get_versions, modpack::get_latest_version, prism::launch_prism, prism::install_prism, admin::login, admin::drop_session, admin::shift_up, admin::shift_down, admin::add_pack, admin::remove_pack, admin::update_pack])
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
greet,
modpack::get_modpacks,
modpack::launch_modpack,
modpack::get_versions,
modpack::get_latest_version,
prism::launch_prism,
prism::install_prism,
admin::login,
admin::drop_session,
admin::shift_up,
admin::shift_down,
admin::add_pack,
admin::remove_pack,
admin::update_pack
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,34 +1,35 @@
use crate::https;
use crate::java; use crate::{java};
use crate::system_dirs::{ use crate::system_dirs::{get_data_directory, get_java_executable, get_local_data_directory, get_prism_executable};
get_data_directory, get_java_executable, get_local_data_directory, get_prism_executable,
};
use crate::util;
use reqwest::IntoUrl;
use serde::de::value::Error;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::fs;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::process::Command;
use std::time::Duration; use std::time::Duration;
use std::{env, thread}; use std::{env, thread};
use std::fs::File;
use std::process::Command;
use std::{io::Cursor, path::PathBuf}; use std::{io::Cursor, path::PathBuf};
use std::io::{Read, Seek, Write};
use reqwest::IntoUrl;
use serde::de::value::Error;
use serde_json::Value;
use serde::Serialize;
use serde::Deserialize;
use crate::util;
use std::fs;
use crate::https;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ModpackEntry { pub struct ModpackEntry{
pub name: String, pub name: String,
pub id: String, pub id: String,
pub last_updated: String, pub last_updated: String
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct VersionEntry { pub struct VersionEntry{
pub Version: String, pub Version: String,
pub File: String, pub File: String,
pub Date: String, pub Date: String
} }
async fn get_modpack_name(id: String) -> String { async fn get_modpack_name(id: String) -> String {
@ -42,101 +43,59 @@ async fn get_modpack_name(id: String) -> String {
return instance_name; return instance_name;
} }
async fn check_modpack_needs_update(id: String) -> bool {
async fn check_modpack_needs_update(id: String) -> bool{
let mut instance_name = get_modpack_name(id.clone()).await; let mut instance_name = get_modpack_name(id.clone()).await;
if !get_local_data_directory() if !get_local_data_directory().join("prism").join("instances").join(&mut instance_name).exists() {
.join("prism")
.join("instances")
.join(&mut instance_name)
.exists()
{
return true; return true;
} }
let versions = get_versions(id).await; let versions = get_versions(id).await;
if !versions.is_ok() { if !versions.is_ok() {
return false; return false;
} }
let versions = versions.unwrap(); let versions = versions.unwrap();
let latest = versions[versions.len() - 1].Version.clone(); let latest = versions[versions.len()-1].Version.clone();
let mut file = File::open( let mut file = File::open(get_local_data_directory().join("prism").join("instances").join(instance_name).join(".minecraft").join("version.txt")).unwrap();
get_local_data_directory()
.join("prism")
.join("instances")
.join(instance_name)
.join(".minecraft")
.join("version.txt"),
)
.unwrap();
let mut current = String::new(); let mut current = String::new();
file.read_to_string(&mut current); file.read_to_string(&mut current);
if latest != current { if latest != current {
return true; return true;
} }
return false; return false;
} }
#[tauri::command] #[tauri::command]
pub async fn launch_modpack(window: tauri::AppHandle, id: String) { pub async fn launch_modpack(window: tauri::Window, id: String){
if check_modpack_needs_update(id.clone()).await { if check_modpack_needs_update(id.clone()).await {
install_modpack(window, id.clone()).await; install_modpack(window, id.clone()).await;
} }
// Launch // Launch
let mut child = Command::new( let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).arg("-l").arg(get_modpack_name(id).await).spawn().unwrap();
get_local_data_directory()
.join("prism")
.join(get_prism_executable()),
)
.arg("-l")
.arg(get_modpack_name(id).await)
.spawn()
.unwrap();
} }
async fn install_modpack(window: tauri::AppHandle, id: String) { async fn install_modpack(window: tauri::Window, id: String){
let versions = get_versions(id.clone()).await.unwrap(); let versions = get_versions(id.clone()).await.unwrap();
let path = env::temp_dir().join(format!("{}.mrpack", get_modpack_name(id.clone()).await)); let path = env::temp_dir().join(format!("{}.mrpack", get_modpack_name(id.clone()).await));
let mut file = File::create(path.clone()).unwrap(); let mut file = File::create(path.clone()).unwrap();
let ftp_path = PathBuf::new() let ftp_path = PathBuf::new().join(id.clone()).join(versions[versions.len()-1].File.clone().as_str());
.join(id.clone())
.join(versions[versions.len() - 1].File.clone().as_str());
println!("Downloading file {}", ftp_path.to_str().unwrap()); println!("Downloading file {}", ftp_path.to_str().unwrap());
//ftp::ftp_retr(Some(window.clone()), ftp_path, &mut file, |window, data, size| util::download_callback(name.clone(), window, data, size)).unwrap(); //ftp::ftp_retr(Some(window.clone()), ftp_path, &mut file, |window, data, size| util::download_callback(name.clone(), window, data, size)).unwrap();
https::download( https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/{}/{}", id.clone(), versions[versions.len()-1].File.clone()), &mut file, get_modpack_name(id.clone()).await).await;
Some(window.clone()), let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).arg("-I").arg(path).spawn().unwrap();
format!(
"https://gitea.piwalker.net/fclauncher/{}/{}",
id.clone(),
versions[versions.len() - 1].File.clone()
),
&mut file,
get_modpack_name(id.clone()).await,
)
.await;
let mut child = Command::new(
get_local_data_directory()
.join("prism")
.join(get_prism_executable()),
)
.arg("-I")
.arg(path)
.spawn()
.unwrap();
loop { loop {
let version_path = get_local_data_directory() let version_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join(".minecraft").join("version.txt");
.join("prism")
.join("instances")
.join(get_modpack_name(id.clone()).await)
.join(".minecraft")
.join("version.txt");
if version_path.clone().exists() { if version_path.clone().exists() {
let mut ver_file = File::open(version_path).unwrap(); let mut ver_file = File::open(version_path).unwrap();
let mut buf = String::new(); let mut buf = String::new();
ver_file.read_to_string(&mut buf).unwrap(); ver_file.read_to_string(&mut buf).unwrap();
if buf == versions[versions.len() - 1].Version.clone().as_str() { if buf == versions[versions.len()-1].Version.clone().as_str() {
break; break;
} }
} }
@ -144,11 +103,7 @@ async fn install_modpack(window: tauri::AppHandle, id: String) {
} }
thread::sleep(Duration::from_secs(1)); thread::sleep(Duration::from_secs(1));
child.kill(); child.kill();
let info_path = get_local_data_directory() let info_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join("mmc-pack.json");
.join("prism")
.join("instances")
.join(get_modpack_name(id.clone()).await)
.join("mmc-pack.json");
let mut info_file = File::open(info_path.clone()).unwrap(); let mut info_file = File::open(info_path.clone()).unwrap();
let info_json: Value = serde_json::from_reader(info_file).unwrap(); let info_json: Value = serde_json::from_reader(info_file).unwrap();
let mut mc_version = "0.0"; let mut mc_version = "0.0";
@ -160,11 +115,7 @@ async fn install_modpack(window: tauri::AppHandle, id: String) {
let java = get_java_version(mc_version); let java = get_java_version(mc_version);
java::install_java(java, window.clone()).await; java::install_java(java, window.clone()).await;
let option_path = get_local_data_directory() let option_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join("instance.cfg");
.join("prism")
.join("instances")
.join(get_modpack_name(id.clone()).await)
.join("instance.cfg");
let mut option_file = File::open(option_path.clone()).unwrap(); let mut option_file = File::open(option_path.clone()).unwrap();
let mut buf = String::new(); let mut buf = String::new();
option_file.read_to_string(&mut buf); option_file.read_to_string(&mut buf);
@ -172,53 +123,18 @@ async fn install_modpack(window: tauri::AppHandle, id: String) {
let mut set = false; let mut set = false;
for line in buf.lines() { for line in buf.lines() {
if line.starts_with("JavaPath=") { if line.starts_with("JavaPath=") {
option_file.write_all( option_file.write_all(format!("JavaPath={}/java-{}-{}/bin/{}\n", get_local_data_directory().join("java").into_os_string().to_str().unwrap().replace("\\", "/"), java, if env::consts::OS == "windows" {"win"} else {"lin"}, get_java_executable()).as_bytes());
format!(
"JavaPath={}/java-{}-{}/bin/{}\n",
get_local_data_directory()
.join("java")
.into_os_string()
.to_str()
.unwrap()
.replace("\\", "/"),
java,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
},
get_java_executable()
)
.as_bytes(),
);
set = true; set = true;
} else { } else {
option_file.write_all(format!("{}\n", line).as_bytes()); option_file.write_all(format!("{}\n",line).as_bytes());
} }
} }
if !set { if !set {
option_file.write_all( option_file.write_all(format!("JavaPath={}/java-{}-{}/bin/{}\n", get_local_data_directory().join("java").into_os_string().to_str().unwrap().replace("\\", "/"), java, if env::consts::OS == "windows" {"win"} else {"lin"}, get_java_executable()).as_bytes());
format!(
"JavaPath={}/java-{}-{}/bin/{}\n",
get_local_data_directory()
.join("java")
.into_os_string()
.to_str()
.unwrap()
.replace("\\", "/"),
java,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
},
get_java_executable()
)
.as_bytes(),
);
option_file.write_all("OverrideJavaLocation=true\n".as_bytes()); option_file.write_all("OverrideJavaLocation=true\n".as_bytes());
option_file.write_all("OverrideJava=true\n".as_bytes()); option_file.write_all("OverrideJava=true\n".as_bytes());
} }
} }
#[tauri::command] #[tauri::command]
@ -228,19 +144,12 @@ pub async fn get_modpacks() -> Vec<ModpackEntry> {
//if modpacks.is_empty() { //if modpacks.is_empty() {
let mut buf = Cursor::new(vec![]); let mut buf = Cursor::new(vec![]);
//ftp::ftp_retr(None, PathBuf::new().join("modpacks.json"), &mut buf, |_, _, _| return); //ftp::ftp_retr(None, PathBuf::new().join("modpacks.json"), &mut buf, |_, _, _| return);
https::download( https::download(None, format!("https://gitea.piwalker.net/fclauncher/modpacks.json"), &mut buf, format!("modpacks.json")).await;
None,
format!("https://gitea.piwalker.net/fclauncher/modpacks.json"),
&mut buf,
format!("modpacks.json"),
)
.await;
buf.rewind(); buf.rewind();
let res = serde_json::from_reader(buf); let res = serde_json::from_reader(buf);
if !res.is_ok() { if !res.is_ok() {
println!("Result not ok!"); println!("Result not ok!");
let paths = let paths = fs::read_dir(get_local_data_directory().join("prism").join("instances")).unwrap();
fs::read_dir(get_local_data_directory().join("prism").join("instances")).unwrap();
for path in paths { for path in paths {
let path = path.unwrap(); let path = path.unwrap();
if fs::metadata(path.path()).unwrap().is_file() { if fs::metadata(path.path()).unwrap().is_file() {
@ -250,19 +159,15 @@ pub async fn get_modpacks() -> Vec<ModpackEntry> {
if name.starts_with(".") { if name.starts_with(".") {
continue; continue;
} }
modpacks.push(ModpackEntry { modpacks.push(ModpackEntry{name: name.clone(), id: name, last_updated: format!("")})
name: name.clone(),
id: name,
last_updated: format!(""),
})
} }
return modpacks.clone(); return modpacks.clone()
} }
let modpacks: Vec<ModpackEntry> = res.unwrap(); let modpacks: Vec<ModpackEntry> = res.unwrap();
//println!("{}", v[0].name); //println!("{}", v[0].name);
//for pack in v.as_array().unwrap() { //for pack in v.as_array().unwrap() {
//modpacks.push(ModpackEntry{name: pack["name"].as_str().unwrap().to_string(), id: pack["id"].as_str().unwrap().to_string(), last_updated: pack["last-updated"].as_str().unwrap().to_string()}); //modpacks.push(ModpackEntry{name: pack["name"].as_str().unwrap().to_string(), id: pack["id"].as_str().unwrap().to_string(), last_updated: pack["last-updated"].as_str().unwrap().to_string()});
//} //}
//} //}
return modpacks.clone(); return modpacks.clone();
@ -270,30 +175,20 @@ pub async fn get_modpacks() -> Vec<ModpackEntry> {
} }
#[tauri::command] #[tauri::command]
pub async fn get_versions(id: String) -> Result<Vec<VersionEntry>, String> { pub async fn get_versions(id: String) -> Result<Vec<VersionEntry>,String> {
let mut versions: Vec<VersionEntry> = Vec::new(); let mut versions: Vec<VersionEntry> = Vec::new();
let mut buf = Cursor::new(vec![]); let mut buf = Cursor::new(vec![]);
//ftp::ftp_retr(None, PathBuf::new().join(id).join("versions.json"), &mut buf, |_, _, _| return); //ftp::ftp_retr(None, PathBuf::new().join(id).join("versions.json"), &mut buf, |_, _, _| return);
https::download( https::download(None, format!("https://gitea.piwalker.net/fclauncher/{}/versions.json", id.clone()), &mut buf, format!("{}/versions.json", id.clone())).await;
None,
format!(
"https://gitea.piwalker.net/fclauncher/{}/versions.json",
id.clone()
),
&mut buf,
format!("{}/versions.json", id.clone()),
)
.await;
buf.rewind(); buf.rewind();
let versions: Vec<VersionEntry> = let versions: Vec<VersionEntry> = serde_json::from_reader(buf).or(Err(format!("Unable to parse json")))?;
serde_json::from_reader(buf).or(Err(format!("Unable to parse json")))?;
//for version in v.as_array().unwrap() { //for version in v.as_array().unwrap() {
//versions.push(VersionEntry{version: version["Version"].as_str().unwrap().to_string(), file: version["File"].as_str().unwrap().to_string(), date: version["Date"].as_str().unwrap().to_string()}); //versions.push(VersionEntry{version: version["Version"].as_str().unwrap().to_string(), file: version["File"].as_str().unwrap().to_string(), date: version["Date"].as_str().unwrap().to_string()});
//} //}
return Ok(versions.clone()); return Ok(versions.clone());
} }
fn get_java_version(mc_version: &str) -> u8 { fn get_java_version(mc_version: &str) -> u8{
let components: Vec<&str> = mc_version.split(".").collect(); let components: Vec<&str> = mc_version.split(".").collect();
let mut java = 8; let mut java = 8;
if components[1] == "17" { if components[1] == "17" {
@ -311,14 +206,15 @@ fn get_java_version(mc_version: &str) -> u8 {
} }
#[tauri::command] #[tauri::command]
pub async fn get_latest_version(id: String) -> Result<String, String> { pub async fn get_latest_version(id: String) -> Result<String, String>{
let versions = get_versions(id).await.unwrap(); let versions = get_versions(id).await.unwrap();
if (versions.len() == 0) { if(versions.len() == 0){
return Ok(format!("")); return Ok(format!(""));
} }
Ok(versions[versions.len() - 1].Version.clone()) Ok(versions[versions.len()-1].Version.clone())
} }
//pub fn create_json(modpacks: Vec<ModpackEntry>) -> Result<serde_json::Value, String> { //pub fn create_json(modpacks: Vec<ModpackEntry>) -> Result<serde_json::Value, String> {
//} //}

View File

@ -0,0 +1,65 @@
use std::{env, fs::File, io::{BufRead, Cursor, Seek, Write}, path::PathBuf, process::Command, str::FromStr};
use flate2::read::GzDecoder;
use tar::Archive;
use tauri::api::file;
use crate::{https, java, system_dirs::{get_local_data_directory, get_prism_executable}, util};
pub fn check_prism() -> bool {
let path = get_local_data_directory().join("prism");
path.exists()
}
#[tauri::command]
pub async fn install_prism(window: tauri::Window){
if check_prism() {
return;
}
java::install_java(21, window.clone()).await;
let mut buff = Cursor::new(vec![]);
let mut total = 0;
//ftp::ftp_retr(Some(window.clone()), path, &mut buff, |window: Option<tauri::Window>, data, size| util::download_callback("Prism Launcher".to_string(),window, data, size)).unwrap();
https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/prism/prism-{}", if env::consts::OS == "windows" {"win.zip"} else {"lin.tar.gz"}), &mut buff, format!("Prism Launcher")).await;
std::fs::create_dir_all(get_local_data_directory().join("prism")).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !archive.unpack(get_local_data_directory().join("prism")).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
} else {
if !zip_extract::extract(buff, get_local_data_directory().join("prism").as_path(), true).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
}
let mut buff = Cursor::new(vec![]);
//ftp::ftp_retr(Some(window.clone()), PathBuf::new().join("prism").join("prismlauncher.cfg"), &mut buff, |_, _, _| return).unwrap();
https::download(None, format!("https://gitea.piwalker.net/fclauncher/prism/prismlauncher.cfg"), &mut buff, format!("prismlauncher.cfg")).await;
buff.rewind();
let mut file = File::create(get_local_data_directory().join("prism").join("prismlauncher.cfg")).unwrap();
loop {
let mut buf = String::new();
let count = buff.read_line(&mut buf).unwrap();
if count == 0 {
break;
}
if buf.starts_with("JavaPath") {
buf = format!("JavaPath={}/java/java-21-{}\n", get_local_data_directory().to_str().unwrap().replace("\\", "/"), if env::consts::OS == "windows" { "win" } else { "lin" });
}else if buf.starts_with("LastHostname") {
buf = format!("LastHostname={}\n", gethostname::gethostname().to_str().unwrap());
}
file.write_all(buf.as_bytes());
}
}
#[tauri::command]
pub fn launch_prism() {
let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).spawn().unwrap();
}

View File

@ -0,0 +1,66 @@
use std::io::prelude::*;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use futures_util::future::BoxFuture;
use futures_util::io::BufReader;
use futures_util::io::Cursor;
use futures_util::FutureExt;
use ssh2::OpenFlags;
use ssh2::Session;
use ssh2::Sftp;
use crate::https;
pub fn connect(username: String, password: String) -> Result<Session, String> {
let tcp = TcpStream::connect("gitea.piwalker.net:22").or(Err(format!("Unable to connect to host")))?;
let mut sess = Session::new().or(Err(format!("Unable to creat stream")))?;
sess.set_tcp_stream(tcp);
sess.handshake().unwrap();
sess.userauth_password(username.as_str(), password.as_str()).or(Err(format!("Invalid username or password")))?;
Ok(sess)
}
pub async fn uplaod(window: Option<tauri::Window>, sess: Session, path: PathBuf, mut reader: impl Read, upload_name: String, total_size: usize) -> Result<(), String>{
let sftp = sess.sftp().or(Err("unable to open sftp session"))?;
let mut file = sftp.create(path.as_path()).or(Err("Unable to open file"))?;
let mut uploaded = 0;
while true {
let mut buf = vec![0u8; 1024*32];
let res = reader.read(&mut buf).unwrap();
if res <= 0 {
if let Some(window) = window.clone() {
window.emit("download_finished", true).or(Err(format!("Unable to signal window!")))?;
}
break;
}
file.write_all(buf.split_at(res).0).unwrap();
uploaded += res;
println!("Uploading {} {}MB / {}MB", upload_name, uploaded/(1024*1024), total_size/(1024*1024));
if let Some(window) = window.clone() {
window.emit("download_progress", https::DownloadStatus{downloaded: uploaded as usize, total: total_size as usize, time_elapsed: 0, download_name: upload_name.clone()}).or(Err(format!("Unable to signal window")))?;
}
}
Ok(())
}
pub async fn mkdir(sess: Session, path: PathBuf) -> Result<(), String>{
let sftp = sess.sftp().or(Err("Unable to open sftp session"))?;
sftp.mkdir(path.as_path(), 0o775).or(Err(format!("Unable to create directory")))?;
Ok(())
}
pub fn rmdir(sess: Session, path: PathBuf) -> BoxFuture<'static, ()>{
async move {
let sftp = sess.sftp().or(Err("Unable to open sftp session")).unwrap();
let dirs = sftp.readdir(path.as_path()).or(Err("unable to stat directory")).unwrap();
for dir in dirs {
if dir.1.is_dir() {
rmdir(sess.clone(), dir.0).await;
}else {
sftp.unlink(dir.0.as_path()).or(Err(format!("Unable to delete file"))).unwrap();
}
}
sftp.rmdir(path.as_path()).or(Err(format!("Unable to delete directory"))).unwrap();
}.boxed()
}

View File

@ -0,0 +1,20 @@
use std::{env, path::{Path, PathBuf}};
use dirs::home_dir;
pub fn get_local_data_directory() -> PathBuf {
dirs::data_local_dir().unwrap().join("FCLauncher")
}
pub fn get_data_directory() -> PathBuf {
dirs::data_dir().unwrap().join("FCLauncher")
}
pub fn get_java_executable() -> String {
return format!("java{}", if env::consts::OS == "windows" { ".exe" } else { "" })
}
pub fn get_prism_executable() -> String {
return format!("{}", if env::consts::OS == "windows" { "prismlauncher.exe" } else { "PrismLauncher" })
}

View File

@ -0,0 +1,30 @@
use std::clone;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
downloaded: usize,
total: usize,
time_elapsed: usize,
download_name: String
}
pub fn download_callback(download_name: String, window: Option<tauri::Window>, count: usize, size: usize){
unsafe{
static mut total: usize = 0;
total += count;
if let Some(window1) = window.clone() {
window1.emit("download_progress", DownloadStatus{downloaded: total, total: size, time_elapsed: 0, download_name: download_name});
}
println!("Downloading {}MB / {}MB", total/(1024*1024), size/(1024*1024));
if count == 0 {
if let Some(window2) = window {
window2.emit("download_finished", true);
}
total = 0;
}
}
}

View File

@ -0,0 +1,59 @@
{
"build": {
"devPath": "../src",
"distDir": "../src",
"withGlobalTauri": true
},
"package": {
"productName": "FCLauncher",
"version": "1.0.4"
},
"tauri": {
"updater": {
"active": true,
"endpoints": [
"https://gitea.piwalker.net/fclauncher/app.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDOEUwMjYxRUU2NEI5RgpSV1NmUytZZUp1RElBN3dEaGhpWG9JZVNQcFlnNFFzaXN0UnBsVmxNeVdWWnJoQmh4cGJRbjN3Ygo="
},
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"dialog": {
"all": false,
"ask": true,
"open": true
},
"process": {
"all": true
}
},
"windows": [
{
"title": "FCLauncher",
"width": 800,
"height": 600,
"resizable": false
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "net.piwalker",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

View File

@ -1,13 +1,9 @@
const { invoke } = window.__TAURI__.tauri; const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event; const { listen } = window.__TAURI__.event;
const { ask, open, message } = window.__TAURI__.dialog; const { ask, open } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished"); const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api'; //import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
var selected_pack = ""; var selected_pack = "";
var update_menu = document.getElementById("update"); var update_menu = document.getElementById("update");
var create_menu = document.getElementById("create"); var create_menu = document.getElementById("create");

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 943 B

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,13 +1,9 @@
const { invoke } = window.__TAURI__.tauri; const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event; const { listen } = window.__TAURI__.event;
const { ask, message } = window.__TAURI__.dialog; const { ask } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished"); const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api'; //import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
document.getElementById("back").addEventListener("click", back); document.getElementById("back").addEventListener("click", back);

View File

@ -1,14 +1,10 @@
const { invoke } = window.__TAURI__.tauri; const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event; const { listen } = window.__TAURI__.event;
const { ask, message } = window.__TAURI__.dialog; const { ask } = window.__TAURI__.dialog;
const { exit } = window.__TAURI__.process; const { exit } = window.__TAURI__.process;
const downBar = document.querySelector(".progressFinished"); const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api'; //import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
const download_progress = listen("download_progress", (progress) => { const download_progress = listen("download_progress", (progress) => {
console.log("Downloading"); console.log("Downloading");
//console.log("Downloaded "+progress.payload.downloaded/(1024*1024) +"MB / " + progress.payload.total/(1024*1024) + "MB"); //console.log("Downloaded "+progress.payload.downloaded/(1024*1024) +"MB / " + progress.payload.total/(1024*1024) + "MB");

View File

@ -1,4 +0,0 @@
build/bin
node_modules
frontend/dist
frontend/wailsjs/go/

View File

@ -1,34 +0,0 @@
package main
import (
"context"
"os"
"path/filepath"
)
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
}
type InstanceManager struct {
instances []Instance
PrismLauncher Prism
ctx context.Context
}
func (i *InstanceManager)SearchInstances() {
i.instances = []Instance{}
dir := i.PrismLauncher.GetInstanceDir()
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ = os.OpenFile()
}
}

View File

@ -1,44 +0,0 @@
package main
import (
"fmt"
"golang.org/x/crypto/ssh"
)
type Admin struct {
conf *ssh.ClientConfig
}
func sftpConnect(conf *ssh.ClientConfig) (ssh.Conn, error) {
return ssh.Dial("tcp", "gitea-svr.piwalker.net:22", conf)
}
func sftpAuth(username string, password string) (*ssh.ClientConfig, error) {
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte("gitea-svr.piwalker.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILkyh7MDvubWw4OzTFbvsUz7gOmOzBq77i5Q86STKqja"));
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.FixedHostKey(key),
}
conn, err := sftpConnect(config)
if err != nil {
return nil, err
}
conn.Close()
return config, nil
}
func (a *Admin) AdminAuth(username string, password string) bool {
conf, err := sftpAuth(username, password)
if err != nil {
fmt.Println("SFTP error: ", err)
a.conf = nil
return false
}
a.conf = conf
return true
}

View File

@ -1,624 +0,0 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zhyee/zipstream"
)
type MrData struct {
FormatVersion int
Game string
VersionId string
Name string
Summary string
Files []MrFile
Dependencies map[string]string
}
type MrFile struct {
Path string
Hashes map[string]string
Env map[string]string
Downloads []string
FileSize int
}
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
MinecraftVersion string
ForgeVersion string
NeoForgeVersion string
FabricVersion string
QuiltVersion string
JavaVersion int
Libraries []string
MainClass string
}
type InstanceManager struct {
instances []Instance
app *App
}
type mmcpack struct {
Components []component
}
type component struct {
Uid string
Version string
}
func (i *InstanceManager) SearchInstances() {
i.instances = []Instance{}
dir, _ := os.UserConfigDir()
dir = filepath.Join(dir, "FCLauncher", "instances")
if _, err := os.Stat(dir); err != nil {
return
}
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ := os.OpenFile(filepath.Join(dir, d.Name(), "instance.json"), os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
instance := Instance{}
json.Unmarshal(buff.Bytes(), &instance)
i.instances = append(i.instances, instance)
}
}
func (i *InstanceManager) checkJavaVersion(instance Instance) {
infoPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "mmc-pack.json")
f, _ := os.OpenFile(infoPath, os.O_RDONLY, 0755)
defer f.Close()
dataStr, _ := io.ReadAll(f)
var data mmcpack
json.Unmarshal(dataStr, &data)
mc_version := "0.0"
for _, comp := range data.Components {
if comp.Uid == "net.minecraft" {
mc_version = comp.Version
break
}
}
fmt.Printf("MC Version: %s", mc_version)
tokensStr := strings.Split(mc_version, ".")
tokens := []int{0, 0, 0}
tokens[0], _ = strconv.Atoi(tokensStr[0])
tokens[1], _ = strconv.Atoi(tokensStr[1])
if len(tokensStr) > 2 {
tokens[2], _ = strconv.Atoi(tokensStr[2])
}
javaVer := 8
if tokens[1] == 17 {
javaVer = 17
} else if tokens[1] == 18 || tokens[1] == 19 {
javaVer = 17
} else if tokens[1] > 19 {
if tokens[1] == 20 && tokens[2] < 5 {
javaVer = 17
} else {
javaVer = 21
}
}
fmt.Printf("Req Java Version: %d", javaVer)
if !i.app.Java.CheckJavaVer(javaVer) {
i.app.Java.InstallJavaVer(javaVer)
}
confPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.cfg")
f, _ = os.OpenFile(confPath, os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
sc := bufio.NewScanner(buff)
f, _ = os.OpenFile(confPath, os.O_CREATE|os.O_RDWR, 0755)
plat := "lin"
exe := "java"
if runtime.GOOS == "windows" {
plat = "win"
exe = "Java.exe"
}
confDir, _ := os.UserConfigDir()
found := false
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "JavaPath=") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
found = true
}
f.WriteString(line + "\n")
}
if !found {
line := fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
f.WriteString(line + "\n")
f.WriteString("OverrideJavaLocation=true\nOverrideJava=true\n")
}
f.Close()
}
func (i *InstanceManager) InstallModpack(modpack Modpack, instanceName string) {
i.app.Status(fmt.Sprintf("Installing %s", modpack.Name))
version := modpack.Versions[len(modpack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(modpack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance := Instance{InstanceName: instanceName, ModpackVersion: version.Version, ModpackId: modpack.Id}
i.instances = append(i.instances, instance)
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) InstallVanilla(version string, instanceName string) {
dir, _ := os.UserConfigDir()
err := DownloadAssets(version, filepath.Join(dir, "FCLauncher", "assets"), *i.app)
if err != nil {
fmt.Printf("Unable to download assets: %s\n", err)
} else {
fmt.Printf("Assets Downloaded")
}
err = DownloadLibraries(version, filepath.Join(dir, "FCLauncher", "lib"), *i.app)
if err != nil {
fmt.Printf("Unable to download libs: %s\n", err)
} else {
fmt.Printf("Libs Downloaded")
}
InstallNatives(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft", "natives"))
DownloadLoggingConfig(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"))
err = DownloadExecutable(version, filepath.Join(dir, "FCLauncher", "bin"), *i.app)
if err != nil {
fmt.Printf("Unable to download binaries: %s\n", err)
} else {
fmt.Printf("Binaries Downloaded")
}
metadata, err := GetVersionMetadata(version)
if err != nil {
fmt.Printf("unable to pull metadata: %s\n", err)
}
err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"), 0755)
if err != nil {
fmt.Printf("unable to create directory: %s\n", err)
}
instance := Instance{InstanceName: instanceName, MinecraftVersion: version, JavaVersion: metadata.JavaVersion.MajorVersion, MainClass: metadata.MainClass}
for _, lib := range metadata.Libraries {
instance.Libraries = append(instance.Libraries, lib.Downloads.Artifact.Path)
}
data, err := json.Marshal(instance)
if err != nil {
fmt.Printf("unable to marshal json data: %s\n", err)
}
f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
if err != nil {
fmt.Printf("unable to open file: %s\n", err)
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
fmt.Printf("unable to write data: %s\n", err)
}
i.instances = append(i.instances, instance)
if !i.app.Java.CheckJavaVer(instance.JavaVersion) {
i.app.Status(fmt.Sprintf("Installing Java Version %d", instance.JavaVersion))
i.app.Java.InstallJavaVer(instance.JavaVersion)
}
i.SearchInstances()
}
func (i *InstanceManager) GetInstances() []string {
names := []string{}
for _, inst := range i.instances {
names = append(names, inst.InstanceName)
}
return names
}
func (i *InstanceManager) CheckUpdate(instance Instance) {
return
i.app.Status("Checking for Updates")
i.app.Modpacks.QuerryModpacks()
pack := i.app.Modpacks.GetModpack(instance.ModpackId)
if pack.Versions[len(pack.Versions)-1].Version == instance.ModpackVersion {
return
}
i.app.Status(fmt.Sprintf("Updating %s", instance.InstanceName))
version := pack.Versions[len(pack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instance.InstanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(pack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance.ModpackVersion = version.Version
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) GetInstance(instance string) (Instance, error) {
instanceObject := Instance{}
found := false
for _, inst := range i.instances {
if inst.InstanceName == instance {
instanceObject = inst
found = true
break
}
}
if !found {
return Instance{}, fmt.Errorf("unable to find instance %s\n", instance)
}
return instanceObject, nil
}
func (i *InstanceManager) LaunchInstance(instance string) {
i.app.Status(fmt.Sprintf("Launching %s", instance))
dir, err := os.UserConfigDir()
if err != nil {
fmt.Printf("unable to get config directory\n")
}
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance\n")
}
execName := "java"
suffix := "lin"
if runtime.GOOS == "windows" {
execName = "Javaw.exe"
suffix = "win"
}
dir = filepath.Join(dir, "FCLauncher")
auth, err := MicrosoftAuth(i.app.Auth)
if err != nil {
fmt.Printf("unable to authenticate: %s\n", err)
return
}
args, err := GetOnlineLaunchArgs(instanceObject.MinecraftVersion, instanceObject, filepath.Join(dir, "lib"), filepath.Join(dir, "bin"), filepath.Join(dir, "assets"), filepath.Join(dir, "instances", instance, "minecraft"), auth)
if err != nil {
fmt.Printf("unable to get launch args: %s\n", err)
}
if instanceObject.ForgeVersion != "" {
args = append(args, "--launchTarget")
args = append(args, "forge_client")
}
fmt.Printf("Args: %+v", args)
child := exec.Command(filepath.Join(dir, "java", fmt.Sprintf("java-%d-%s", instanceObject.JavaVersion, suffix), "bin", execName), args...)
child.Dir = filepath.Join(dir, "instances", instance, "minecraft")
wruntime.WindowHide(i.app.Ctx)
data, err := child.CombinedOutput()
wruntime.WindowShow(i.app.Ctx)
fmt.Printf("Command Output: %s\n", data)
}
func (i *InstanceManager) InstallFabric(instance string, fabricVersion string) {
i.app.Status("Installing Fabric")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetFabricMetadata(instanceObject.MinecraftVersion, fabricVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
fmt.Printf("Inspecing path %s with %d tokens\n", path, len(tokens))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.FabricVersion = fabricVersion
dir, _ := os.UserConfigDir()
InstallFabricLibs(instanceObject.MinecraftVersion, fabricVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallQuilt(instance string, quiltVersion string) {
i.app.Status("Installing Quilt")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetQuiltMetadata(instanceObject.MinecraftVersion, quiltVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Hashed.Maven), ProcessMavenFilename(metadata.Hashed.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.QuiltVersion = quiltVersion
dir, _ := os.UserConfigDir()
InstallQuiltLibs(instanceObject.MinecraftVersion, quiltVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallForge(instance string, forgeVersion string) {
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance: %s\n", err)
}
installData, err := GetForgeInstallData(instanceObject.MinecraftVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
dir, _ := os.UserConfigDir()
InstallForgeLibs(instanceObject.MinecraftVersion, forgeVersion, filepath.Join(dir, "FCLauncher", "lib"))
instanceObject.ForgeVersion = forgeVersion
outer:
for _, lib := range installData.Libraries {
tokens := strings.Split(lib.Downloads.Artifact.Path, string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, lib.Downloads.Artifact.Path)
for ind, path := range instanceObject.Libraries {
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue outer
}
}
}
instanceObject.MainClass = installData.MainClass
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) ImportModpack(modpack Modpack, name string) {
i.app.Status(fmt.Sprintf("Downloading %s", modpack.Name))
buff := new(bytes.Buffer)
err := HttpDownload(filepath.Join(modpack.Id, modpack.Versions[len(modpack.Versions)-1].File), buff, i.app.Ctx)
if err != nil {
fmt.Printf("Unable to download modpack file: %s\n", err)
return
}
i.ImportMrpack(buff, name)
}
func (i *InstanceManager) ImportMrpack(data io.Reader, name string) {
dir, _ := os.UserConfigDir()
InstancePath := filepath.Join(dir, "FCLauncher", "instances", name, "minecraft")
zr := zipstream.NewReader(data)
mrdata := MrData{}
i.app.Status("Unpacking modpack File")
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
fmt.Printf("Error unpacking modpack file: %s\n", err)
break
}
if entry.Name == "modrinth.index.json" {
i.app.Status("Loading metadata")
file, _ := entry.Open()
data, _ := io.ReadAll(file)
json.Unmarshal(data, &mrdata)
} else {
i.app.Status(fmt.Sprintf("Unpacking %s", entry.Name))
prefix := strings.Split(entry.Name, "/")[0]
if prefix == "overrides" || prefix == "client-overrides" {
path := strings.SplitN(entry.Name, "/", 2)[1]
if entry.IsDir() {
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, path))
if _, err := os.Stat(filepath.Join(InstancePath, path)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, path), 0755)
}
} else {
zf, _ := entry.Open()
defer zf.Close()
fileDir := ""
tokens := strings.Split(path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, fileDir))
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, zf)
}
}
}
}
i.InstallVanilla(mrdata.Dependencies["minecraft"], name)
if mrdata.Dependencies["forge"] != "" {
fmt.Printf("Forge not implemented!")
//implement forge
} else if mrdata.Dependencies["neoforge"] != "" {
fmt.Printf("Neoforge not implemented!")
//implement neoforge
} else if mrdata.Dependencies["fabric-loader"] != "" {
i.InstallFabric(name, mrdata.Dependencies["fabric-loader"])
} else if mrdata.Dependencies["quilt-loader"] != "" {
i.InstallQuilt(name, mrdata.Dependencies["quilt-loader"])
}
i.app.Status("Downloading Mods")
for _, f := range mrdata.Files {
fmt.Printf("Downloading %s\n", f.Path)
i.app.Status(fmt.Sprintf("Downloading %s", f.Path))
resp, err := http.Get(f.Downloads[0])
if err != nil {
fmt.Printf("Unable to download file %s\n", err)
continue
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(i.app.Ctx, "download", downloaded, f.FileSize)
}
fileDir := ""
tokens := strings.Split(f.Path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, f.Path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, buff)
wruntime.EventsEmit(i.app.Ctx, "download_complete")
}
}
func (i *InstanceManager) OpenInstanceFolder(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
dir, _ := os.UserConfigDir()
openbrowser(filepath.Join(dir, "FCLauncher", "instances", instance, "minecraft"))
}
func (i *InstanceManager) DeleteInstance(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
return
}
dir, _ := os.UserConfigDir()
os.RemoveAll(filepath.Join(dir, "FCLauncher", "instances", instance))
i.SearchInstances()
}

View File

@ -1,120 +0,0 @@
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/zhyee/zipstream"
)
type JavaManager struct {
app *App
}
func (JavaManager)CheckJavaVer(version int) bool{
suffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path, _ := os.UserConfigDir()
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
_, err := os.Stat(path)
if err == nil {
return true
}
return false
}
func (j *JavaManager)InstallJavaVer(version int) {
suffix := "lin.tar.gz"
if runtime.GOOS == "windows" {
suffix = "win.zip"
}
buff := new(bytes.Buffer)
HttpDownload("java/"+fmt.Sprintf("java-%d-%s", version, suffix), buff, j.app.Ctx)
path, _ := os.UserConfigDir()
suffix = "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
os.MkdirAll(path, 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, strings.SplitN(entry.Name, "/", 2)[1])
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, strings.SplitN(header.Name, string(os.PathSeparator), 2)[1])
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
}

View File

@ -1,63 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"time"
)
type Modpack struct {
Name string
Id string
Last_updated string
Versions []Version
}
type Version struct {
Version string
Data time.Time
File string
}
type ModpackManager struct {
app *App
Modpacks []Modpack
}
func (m *ModpackManager) QuerryModpacks() {
m.Modpacks = []Modpack{}
buff := new(bytes.Buffer)
err := HttpDownload("modpacks.json", buff, nil)
if err != nil {
fmt.Printf("HTTP error: %s\n", err)
return
}
err = json.Unmarshal(buff.Bytes(), &m.Modpacks)
if err != nil {
return
}
for ind, pack := range m.Modpacks {
buff = new(bytes.Buffer)
err = HttpDownload(pack.Id+"/versions.json", buff, nil)
if err != nil {
continue
}
json.Unmarshal(buff.Bytes(), &pack.Versions)
m.Modpacks[ind] = pack
}
}
func (m *ModpackManager) GetModpacks() []Modpack {
return m.Modpacks
}
func (m *ModpackManager) GetModpack(id string) Modpack {
for _, pack := range m.Modpacks {
if pack.Id == id {
return pack
}
}
return Modpack{}
}

View File

@ -1,170 +0,0 @@
package main
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Prism struct {
app *App
}
func (Prism) CheckInstalled() bool {
path, _ := os.UserConfigDir()
_, err := os.Stat(filepath.Join(path, "FCLauncher", "prism"))
if err == nil {
return true
} else {
return false
}
}
func (p *Prism) Install() {
suffix := "lin.tar.gz"
shortSuffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win.zip"
shortSuffix = "win"
}
buff := new(bytes.Buffer)
HttpDownload("prism/prism-"+suffix, buff, p.app.Ctx)
path, _ := os.UserConfigDir()
os.MkdirAll(filepath.Join(path, "FCLauncher", "prism"), 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, "FCLauncher", "prism", entry.Name)
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, "FCLauncher", "prism", header.Name)
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
dir, _ := os.UserConfigDir()
buff = new(bytes.Buffer)
HttpDownload("prism/prismlauncher.cfg", buff, nil)
scanner := bufio.NewScanner(buff)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "prism", "prismlauncher.cfg"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "JavaPath") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-21-%s", strings.ReplaceAll(dir, "\\", "/"), shortSuffix)
}
if strings.HasPrefix(line, "LastHostname") {
host, _ := os.Hostname()
line = fmt.Sprintf("LastHostname=%s", host)
}
f.WriteString(line + "\n")
}
}
func (Prism)GetInstanceDir() string{
dir, _ := os.UserConfigDir()
return filepath.Join(dir, "FCLauncher", "prism", "instances")
}
func (Prism)getExecutableName() string {
execName := "PrismLauncher"
if runtime.GOOS == "windows" {
execName = "prismlauncher.exe"
}
return execName
}
func (p *Prism)ImportModpack(path string) {
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-I", path)
child.Start()
tokens := strings.Split(path, string(os.PathSeparator))
versionPath := filepath.Join(dir, "FCLauncher", "prism", "instances",strings.Split(tokens[len(tokens)-1], ".")[0], ".minecraft", "version.txt")
for {
time.Sleep(time.Second * 3)
if _, err := os.Stat(versionPath); err == nil {
break
}
}
child.Process.Kill()
}
func (p *Prism)LaunchInstance(instance Instance) {
p.app.Status(fmt.Sprintf("Launching %s", instance.InstanceName))
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-l", instance.InstanceName)
child.Start()
}

View File

@ -1,16 +0,0 @@
# README
## About
This is the official Wails Svelte-TS template.
## Live Development
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools.
## Building
To build a redistributable, production mode package, use `wails build`.

View File

@ -1,158 +0,0 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/inconshreveable/go-update"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
const client_id string = "9305aeb8-5ecb-4e7a-b28f-c33aefcfbd8d"
const client_version string = "0.0.7"
type LauncherMetadata struct {
Schema_Version string
Version string
Desc string
Downloads map[string]string
}
// App struct
type App struct {
Ctx context.Context
PrismLauncher Prism
Java JavaManager
Instance InstanceManager
Modpacks ModpackManager
Auth authenticationResp
}
// NewApp creates a new App application struct
func NewApp() *App {
a := &App{}
a.Java = JavaManager{app: a}
a.Instance = InstanceManager{app: a}
a.Modpacks = ModpackManager{app: a}
return a
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.Ctx = ctx
}
// Greet returns a greeting for the given name
func openbrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func (a *App) CheckPrerequisites() {
buff := new(bytes.Buffer)
err := HttpDownload("launcher.json", buff, nil)
fmt.Printf("Starting\n")
if err == nil {
data, _ := io.ReadAll(buff)
meta := LauncherMetadata{}
json.Unmarshal(data, &meta)
if client_version != meta.Version {
//Update available!
val, _ := wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.QuestionDialog, Title: "Update!", Message: fmt.Sprintf("There is an update available:\n\n%s -> %s\n\nUpdate Description:\n\n%s\n\nWould you like to update?\n", client_version, meta.Version, meta.Desc)})
if val == "Yes" {
//run the update
fmt.Printf("Updating\n")
buff := new(bytes.Buffer)
HttpDownload(meta.Downloads[runtime.GOOS], buff, nil)
executable, _ := os.Executable()
err := update.Apply(buff, update.Options{})
if err != nil {
fmt.Printf("Error!")
}
child := exec.Command(executable)
err = child.Start()
if err != nil {
fmt.Printf("Unable to launch: %s\n", err)
}
wruntime.Quit(a.Ctx)
}
}
}
a.Status("Querrying Existing Instances")
a.Instance.SearchInstances()
a.Status("Pulling Modpacks")
a.Modpacks.QuerryModpacks()
a.Status("Logging in with Microsoft")
dir, _ := os.UserConfigDir()
authenticated := false
if _, err := os.Stat(filepath.Join(dir, "FCLauncher", "authentication.json")); err == nil {
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
json.Unmarshal(data, &a.Auth)
a.Auth, err = TokenRefresh(*a, a.Auth)
if err == nil {
authenticated = true
} else {
fmt.Printf("token reauth failed, requesting device code: %s\n", err)
}
}
if !authenticated {
var err error
//a.Auth, err = AuthCode(*a)
a.Auth, err = OAuth2(*a)
if err != nil {
fmt.Printf("Authentication Error: %s\n", err)
return
}
}
os.MkdirAll(filepath.Join(dir, "FCLauncher"), 0755)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
defer f.Close()
data, _ := json.Marshal(a.Auth)
f.Write(data)
}
func (App) GetVersions() ([]string, error) {
manifest, err := GetVersionManifest()
if err != nil {
fmt.Printf("Manifest Error: %s\n", err)
return []string{}, err
}
versions := []string{}
for _, version := range manifest.Versions {
versions = append(versions, version.Id)
}
return versions, nil
}
func (a *App) Status(status string) {
fmt.Printf("LOG: %s\n", status)
wruntime.EventsEmit(a.Ctx, "status", status)
}

View File

@ -1,268 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type LauncherAuth struct {
Id string
Name string
Token string
}
type McProfile struct {
Id string
Name string
}
type devcodeResp struct {
User_code string
Device_code string
Verification_uri string
Expires_in string
Interval int
Message string
}
type authenticationResp struct {
Access_token string
Token_type string
Refresh_token string
Expires_in string
Error string
Error_description string
}
type xboxAuthProperties struct {
AuthMethod string
SiteName string
RpsTicket string
}
type xboxAuthRequest struct {
Properties xboxAuthProperties
RelyingParty string
TokenType string
}
type xboxDisplayClaim struct {
Uhs string
Gtg string
Xid string
Agg string
Usr string
Utr string
Prv string
}
type xboxDisplayClaims struct {
Xui []xboxDisplayClaim
}
type xboxAuthResponse struct {
IssueInstant time.Time
NotAfter time.Time
Token string
DisplayClaims xboxDisplayClaims
}
type XSTSProperties struct {
SandboxId string
UserTokens []string
}
type XSTSRequest struct {
Properties XSTSProperties
RelyingParty string
TokenType string
}
type McAuthRequest struct {
Xtoken string `json:"xtoken"`
Platform string `json:"platform"`
}
type McAuthResponse struct {
Username string
Access_token string
Expires_in string
Token_type string
}
func getHTTPRedirect(w http.ResponseWriter, r *http.Request, srv *http.Server, code *string) {
r.ParseForm()
fmt.Printf("Response Code: %s\n", r.Form.Get("code"))
if r.Form.Get("code") != "" {
*code = r.Form.Get("code")
io.WriteString(w, "You can now close this window and return to the application.")
} else {
srv.Shutdown(r.Context())
}
}
func AuthCode(a App) (authenticationResp, error) {
authentication := authenticationResp{}
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", url.Values{
"client_id": {client_id},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authentication, fmt.Errorf("Unable to request device code: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return authentication, fmt.Errorf("Unable to request device code: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
codeResp := devcodeResp{}
json.Unmarshal(data, &codeResp)
//display message
fmt.Printf("resp: %s\n", data)
openbrowser(codeResp.Verification_uri)
wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.InfoDialog, Title: "Authentication", Message: codeResp.Message + ". The code has been automatically coppied to the clipboard."})
wruntime.ClipboardSetText(a.Ctx, codeResp.Device_code)
ticker := time.NewTicker(time.Second * time.Duration(codeResp.Interval))
for range ticker.C {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {codeResp.Device_code},
})
if err != nil {
return authentication, fmt.Errorf("Authentication request error %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
if authentication.Error == "" {
return authentication, nil
}
}
return authentication, fmt.Errorf("Unknown error")
}
func OAuth2(a App) (authenticationResp, error) {
code := "code"
srv := http.Server{Addr: ":5000"}
authentication := authenticationResp{}
verifier := make([]byte, 128)
rand.Read(verifier)
verifier_string := base64.RawURLEncoding.EncodeToString(verifier)
challenge := sha256.Sum256([]byte(verifier_string))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { getHTTPRedirect(w, r, &srv, &code) })
openbrowser("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=" + client_id + "&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A5000&response_mode=query&scope=XboxLive.signin&state=12345&code_challenge=" + base64.RawURLEncoding.EncodeToString(challenge[:]) + "&code_challenge_method=S256&prompt=login")
srv.ListenAndServe()
fmt.Printf("continuing auth\n")
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": {"http://127.0.0.1:5000"},
"code_verifier": {verifier_string},
"client_id": {client_id},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to request token: %e", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
//fmt.Printf("auth data: %s\n", data)
return authentication, nil
}
func TokenRefresh(app App, auth authenticationResp) (authenticationResp, error) {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"refresh_token"},
"refresh_token": {auth.Refresh_token},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to refresh token: %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
authResp := authenticationResp{}
json.Unmarshal(data, &authResp)
if authResp.Error != "" {
return authResp, fmt.Errorf("unable to request new token: %s", authResp.Error_description)
}
return authResp, nil
}
func MicrosoftAuth(auth authenticationResp) (LauncherAuth, error) {
//Xbox Live Auth
req, _ := json.Marshal(xboxAuthRequest{Properties: xboxAuthProperties{AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: "d=" + auth.Access_token}, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"})
client := http.Client{}
httpreq, _ := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewBuffer(req))
httpreq.Header.Add("x-xbl-contract-version", "1")
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpResp, err := client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %s\n", httpResp.Status)
}
d, _ := io.ReadAll(httpResp.Body)
xblAuth := xboxAuthResponse{}
json.Unmarshal(d, &xblAuth)
xstsData, _ := json.Marshal(XSTSRequest{Properties: XSTSProperties{SandboxId: "RETAIL", UserTokens: []string{xblAuth.Token}}, RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"})
httpXstsReq, _ := http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(xstsData))
httpXstsReq.Header.Add("Content-Type", "application/json")
httpResp, err = client.Do(httpXstsReq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %s\n", httpResp.Status)
}
d, _ = io.ReadAll(httpResp.Body)
mcApi := xboxAuthResponse{}
json.Unmarshal(d, &mcApi)
mcAuthData, _ := json.Marshal(McAuthRequest{Xtoken: "XBL 3.0 x=" + mcApi.DisplayClaims.Xui[0].Uhs + ";" + mcApi.Token, Platform: "PC_LAUNCHER"})
httpReqMC, _ := http.NewRequest("POST", "https://api.minecraftservices.com/launcher/login", bytes.NewBuffer(mcAuthData))
httpReqMC.Header.Add("Content-Type", "application/json")
httpReqMC.Header.Add("Accept", "application/json")
resp, err := client.Do(httpReqMC)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %s\n", resp.Status)
}
d, _ = io.ReadAll(resp.Body)
mcAuth := McAuthResponse{}
json.Unmarshal(d, &mcAuth)
httpreq, err = http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", new(bytes.Buffer))
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpreq.Header.Add("Authorization", "Bearer "+mcAuth.Access_token)
resp, _ = client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
profile := McProfile{}
json.Unmarshal(data, &profile)
return LauncherAuth{Id: profile.Id, Name: profile.Name, Token: mcAuth.Access_token}, nil
}

View File

@ -1,35 +0,0 @@
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.

View File

@ -1,68 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -1,63 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,15 +0,0 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@ -1,115 +0,0 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
AccessControl::GrantOnFile "$INSTDIR" "(BU)" "FullAccess"
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@ -1,236 +0,0 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "fclauncher"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
!macroend
!macro wails.unassociateFiles
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@ -1,145 +0,0 @@
package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type Fabric struct {
}
type FabricDefinition struct {
Separator string
Build int
Maven string
Version string
Stable bool
}
type FabricLibrary struct {
Name string
Url string
Sha1 string
}
type FabricLibraries struct {
Client []FabricLibrary
Common []FabricLibrary
Server []FabricLibrary
}
type FabricMeta struct {
Version int
Libraries FabricLibraries
MainClass map[string]string
}
type FabricVersion struct {
Loader FabricDefinition
Intermediary FabricDefinition
LauncherMeta FabricMeta
}
func (Fabric) GetFabricVersions(mcVersion string) ([]FabricVersion, error) {
resp, err := http.Get("https://meta.fabricmc.net/v2/versions/loader/" + mcVersion)
if err != nil {
return []FabricVersion{}, fmt.Errorf("Unable to pull fabric version manifest: %s\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
versions := []FabricVersion{}
json.Unmarshal(data, &versions)
return versions, nil
}
func GetFabricMetadata(mcVersion string, fabricVersion string) (FabricVersion, error) {
versions, err := Fabric{}.GetFabricVersions(mcVersion)
if err != nil {
return FabricVersion{}, fmt.Errorf("unable to download versions manifest: %e\n", err)
}
for _, version := range versions {
if version.Loader.Version == fabricVersion {
return version, nil
}
}
return FabricVersion{}, fmt.Errorf("Unable to find requested version.\n")
}
func InstallLib(lib FabricLibrary, libDir string, a *App) {
a.Status(fmt.Sprintf("Checking %s\n", lib.Name))
path := filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
if _, err := os.Stat(filepath.Join(libDir, path)); err == nil {
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Sha1 {
return
}
}
a.Status(fmt.Sprintf("Downloading %s\n", lib.Name))
unixPath := strings.ReplaceAll(path, "\\", "/")
resp, err := http.Get(lib.Url + unixPath)
if err != nil {
return
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength)
}
os.MkdirAll(filepath.Join(libDir, ProcessMavenPath(lib.Name)), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, buff)
wruntime.EventsEmit(a.Ctx, "download_complete")
}
func InstallFabricLibs(mcVersion string, fabricVersion string, libDir string, a *App) {
metadata, _ := GetFabricMetadata(mcVersion, fabricVersion)
for _, lib := range metadata.LauncherMeta.Libraries.Client {
InstallLib(lib, libDir, a)
}
for _, lib := range metadata.LauncherMeta.Libraries.Common {
InstallLib(lib, libDir, a)
}
InstallLib(FabricLibrary{Name: metadata.Loader.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
InstallLib(FabricLibrary{Name: metadata.Intermediary.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
}
func ProcessMavenPath(maven string) string {
tokens := strings.Split(maven, ":")
path := filepath.Join(strings.Split(tokens[0], ".")...)
pack := tokens[1]
version := tokens[2]
path = filepath.Join(path, pack, version)
return path
}
func ProcessMavenFilename(maven string) string {
tokens := strings.Split(maven, ":")
pack := tokens[1]
version := tokens[2]
return pack + "-" + version + ".jar"
}

View File

@ -1,185 +0,0 @@
package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Forge struct{}
type ForgeVersion struct {
Version string
Time string
Url string
}
type ForgeLibraryArtifact struct {
Path string
Url string
Sha1 string
}
type ForgeLibraryDownload struct {
Artifact ForgeLibraryArtifact
}
type ForgeLibrary struct {
Name string
Downloads ForgeLibraryDownload
}
type ForgeInstallData struct {
Id string
Time time.Time
ReleaseTime time.Time
InheritsFrom string
Type string
MainClass string
Libraries []ForgeLibrary
}
func parseForgeVersions(html string) []ForgeVersion {
lines := strings.Split(html, "\n")
parsing := false
foundTR := false
buff := ""
versions := []ForgeVersion{}
for _, line := range lines {
if strings.Contains(line, "<tbody>") {
parsing = true
} else if strings.Contains(line, "</tbody>") {
parsing = false
} else if parsing {
if strings.Contains(line, "<tr>") {
buff = ""
foundTR = true
} else if strings.Contains(line, "</tr>") {
foundTR = false
versions = append(versions, parseForgeVersion(buff))
} else if foundTR {
buff += line + "\n"
}
}
}
return versions
}
func parseForgeVersion(html string) ForgeVersion {
lines := strings.Split(html, "\n")
version := ForgeVersion{}
for ind, line := range lines {
if strings.Contains(line, "<td class=\"download-version\">") {
version.Version = strings.TrimSpace(lines[ind+1])
} else if strings.Contains(line, "<td class=\"download-time\"") {
version.Time = strings.Split(strings.Split(line, "<td class=\"download-time\" title=\"")[1], "\">")[0]
} else if strings.Contains(line, "https://adfoc.us") && strings.Contains(line, "installer.jar") {
version.Url = strings.Split(strings.Split(line, "&url=")[1], "\">")[0]
}
}
return version
}
func (Forge) GetForgeVersions(mcVersion string) ([]ForgeVersion, error) {
resp, err := http.Get(fmt.Sprintf("https://files.minecraftforge.net/net/minecraftforge/forge/index_%s.html", mcVersion))
if err != nil {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %s", err)
}
data, _ := io.ReadAll(resp.Body)
return parseForgeVersions(string(data)), nil
}
func GetForgeInstallDataFromVersion(version ForgeVersion) (ForgeInstallData, error) {
resp, err := http.Get(version.Url)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %s", resp.Status)
}
zs := zipstream.NewReader(resp.Body)
for {
entry, err := zs.GetNextEntry()
if err == io.EOF {
break
}
if entry.Name != "version.json" {
continue
}
f, _ := entry.Open()
defer f.Close()
data, _ := io.ReadAll(f)
installData := ForgeInstallData{}
json.Unmarshal(data, &installData)
return installData, nil
}
return ForgeInstallData{}, fmt.Errorf("unable to find version.json")
}
func GetForgeInstallData(mcVersion string, forgeVersion string) (ForgeInstallData, error) {
versions, err := Forge{}.GetForgeVersions(mcVersion)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("failed to pull forge versions: %e", err)
}
for _, version := range versions {
if version.Version == forgeVersion {
return GetForgeInstallDataFromVersion(version)
}
}
return ForgeInstallData{}, fmt.Errorf("unable to find the requested version")
}
func InstallForgeLibs(mcVersion string, forgeVersion string, libDir string) {
installData, err := GetForgeInstallData(mcVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
for _, lib := range installData.Libraries {
if _, err := os.Stat(filepath.Join(libDir, lib.Downloads.Artifact.Path)); err == nil {
if f, err := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_RDONLY, 0755); err == nil {
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 {
continue
}
}
}
resp, err := http.Get(lib.Downloads.Artifact.Url)
if err != nil {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, resp.Status)
continue
}
tokens := strings.Split(lib.Downloads.Artifact.Path, "/")
path := ""
for ind, token := range tokens {
if ind == len(tokens)-1 {
break
}
path = filepath.Join(path, token)
}
os.MkdirAll(filepath.Join(libDir, path), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, resp.Body)
}
}

View File

@ -1,5 +0,0 @@
{
"recommendations": [
"svelte.svelte-vscode"
]
}

View File

@ -1,65 +0,0 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/)
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its
serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less,
and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account
the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the
other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte
project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been
structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash
references keeps the default TypeScript setting of accepting type information from the entire workspace, while also
adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to
install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of
JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds:
not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing
JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr`
and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the
details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be
replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>fclauncher</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
}

View File

@ -1 +0,0 @@
48cb20b8d107dab0a7876a449352234a

Some files were not shown because too many files have changed in this diff Show More