feat: process management + os specific settings + proxied events + launch half done

This commit is contained in:
LynithDev 2025-04-17 20:14:25 +02:00
parent a8eb841bd7
commit 71d584e5a3
44 changed files with 1001 additions and 315 deletions

24
Cargo.lock generated
View file

@ -2542,6 +2542,28 @@ dependencies = [
"autocfg",
]
[[package]]
name = "merge"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e520ba58faea3487f75df198b1d079644ec226ea3b0507d002c6fa4b8cf93a"
dependencies = [
"merge_derive",
"num-traits",
]
[[package]]
name = "merge_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8f8ce6efff81cbc83caf4af0905c46e58cb46892f63ad3835e81b47eaf7968"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -3020,6 +3042,7 @@ dependencies = [
"indicatif",
"interpulse",
"md-5",
"merge",
"murmur2",
"onelauncher_entity",
"onelauncher_gamemode",
@ -3063,6 +3086,7 @@ version = "0.2.0-alpha.2"
dependencies = [
"chrono",
"interpulse",
"merge",
"onelauncher_macro",
"sea-orm",
"serde",

View file

@ -149,6 +149,7 @@ async-trait = { version = "0.1" }
whoami = { version = "1.5" }
cc = { version = "1.1" }
libc = { version = "0.2" }
merge = { version = "0.2" }
# macro
syn = { version = "2" }

View file

@ -67,6 +67,7 @@ url = { workspace = true }
async-trait = { workspace = true }
paste = { workspace = true }
byteorder = { workspace = true }
merge = { workspace = true }
# integration
discord-rich-presence = { workspace = true }

View file

@ -12,7 +12,7 @@ async fn main() -> LauncherResult<()> {
}
println!("creating cluster");
let cluster = api::cluster::create_cluster(
let cluster = &mut api::cluster::create_cluster(
"Test Cluster",
"1.8.9",
GameLoader::Vanilla,
@ -23,7 +23,7 @@ async fn main() -> LauncherResult<()> {
println!("cluster created: {cluster:#?}");
println!("preparing cluster for launch");
api::cluster::prepare_cluster(&cluster).await?;
api::cluster::prepare_cluster(cluster).await?;
Ok(())
}

View file

@ -34,7 +34,7 @@ pub async fn insert_cluster(
/// Updates an existing cluster in the database.
pub async fn update_cluster_by_id<B>(
id: i32,
id: u64,
block: B,
) -> LauncherResult<clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
@ -48,8 +48,25 @@ where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveMo
Ok(model)
}
pub async fn update_cluster<B>(
cluster: &mut clusters::Model,
block: B,
) -> LauncherResult<&mut clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
let state = State::get().await?;
let db = &state.db;
let model = cluster.clone().into_active_model();
let model = block(model).await?;
let model = model.update(db).await?;
*cluster = model;
Ok(cluster)
}
/// Deletes a cluster by its ID from the **database**.
pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> {
pub async fn delete_cluster_by_id(id: u64) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
@ -65,7 +82,7 @@ pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> {
}
/// Gets a cluster by its ID from the database.
pub async fn get_cluster_by_id(id: i32) -> LauncherResult<Option<clusters::Model>> {
pub async fn get_cluster_by_id(id: u64) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;

View file

@ -1,3 +1,4 @@
use chrono::Utc;
use interpulse::api::minecraft::VersionInfo;
use onelauncher_entity::cluster_stage::ClusterStage;
use onelauncher_entity::{clusters, java_versions};
@ -26,6 +27,10 @@ use super::ingress::init_ingress;
pub enum ClusterError {
#[error("version '{0}' was not found")]
InvalidVersion(String),
#[error("failed to imply java version")]
MissingJavaVersion,
#[error("cluster is in downloading stage")]
ClusterDownloading,
}
#[must_use]
@ -97,7 +102,7 @@ pub async fn create_cluster(
}
/// Make a cluster playable (installs all necessary game files and dependencies required to play)
pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&clusters::Model> {
const INGRESS_TOTAL: f64 = 100.0;
const INGRESS_TASKS: f64 = 4.0;
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
@ -106,7 +111,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
if cluster.stage == ClusterStage::Ready {
tracing::info!("cluster is already ready");
return Ok(());
return Ok(cluster);
}
let ingress_id = init_ingress(
@ -118,7 +123,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
)
.await?;
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::Downloading);
Ok(cluster)
})
@ -128,7 +133,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
// TASK 1
ingress_id.set_ingress_message("fetching data").await?;
let setting_profile =
setting_profiles::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
let state = State::get().await?;
let mut metadata = state.metadata.write().await;
@ -160,22 +165,11 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
version
} else {
// TODO: java runtime not found - should prob download
let java = java::locate_java().await?;
let Some(ver) = &version_info.java_version else {
return Err(ClusterError::MissingJavaVersion.into());
};
if java.is_empty() {
tracing::warn!("No Java installations found");
return Ok(());
}
for (path, info) in java {
java::dao::insert_java(path, info).await?;
}
java::get_recommended_java(&version_info, Some(&setting_profile)).await?
.ok_or_else(|| {
anyhow::anyhow!("No Java installations found, please install a Java runtime")
})?
java::prepare_java(ver.major_version).await?
};
// TASK 3
@ -189,7 +183,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
if let Err(err) = result {
tracing::error!("failed to prepare cluster: {}", err);
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::NotReady);
Ok(cluster)
})
@ -197,14 +191,14 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
return Err(err);
}
dao::update_cluster_by_id(cluster.id, async |mut cluster| {
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::Ready);
Ok(cluster)
})
.await?;
tracing::debug!("cluster is ready");
Ok(())
Ok(cluster)
}
/// Run forge processors
@ -308,3 +302,14 @@ async fn run_forge_processors(
Ok(())
}
pub async fn update_playtime(id: u64, duration: i64) -> LauncherResult<clusters::Model> {
dao::update_cluster_by_id(id, async |mut cluster| {
let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default();
cluster.overall_played = Set(Some(overall_played + duration));
cluster.last_played = Set(Some(Utc::now().timestamp()));
Ok(cluster)
}).await
}

View file

@ -26,7 +26,7 @@ impl SyncAction {
/// Syncs all clusters in the database. Returns a list of cluster IDs that are missing.
#[tracing::instrument]
pub async fn sync_clusters() -> LauncherResult<Vec<i32>> {
pub async fn sync_clusters() -> LauncherResult<Vec<u64>> {
let clusters = dao::get_all_clusters().await?;
let mut missing_ids = Vec::new();
@ -92,7 +92,7 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncActio
/// Syncs the cluster with the database.
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster_by_id(id: i32) -> LauncherResult<SyncAction> {
pub async fn sync_cluster_by_id(id: u64) -> LauncherResult<SyncAction> {
let cluster = dao::get_cluster_by_id(id)
.await?
.ok_or(DaoError::NotFound)?;

View file

@ -0,0 +1,69 @@
use std::sync::Arc;
use onelauncher_entity::{cluster_stage::ClusterStage, prelude::model::*};
use tokio::sync::RwLock;
use merge::Merge;
use crate::{api::{cluster::{prepare_cluster, ClusterError}, game::metadata, java::{self, JavaError}, setting_profiles::{dao::get_profile_by_name, get_global_profile}}, error::LauncherResult, store::{credentials::MinecraftCredentials, processes::Process, State}, utils::io};
#[tracing::instrument]
pub async fn launch_minecraft(
cluster: &mut Cluster,
creds: &MinecraftCredentials,
) -> LauncherResult<Arc<RwLock<Process>>> {
if cluster.stage.is_downloading() {
return Err(ClusterError::ClusterDownloading.into());
} else if cluster.stage == ClusterStage::NotReady {
prepare_cluster(cluster).await?;
}
let mut settings = get_global_profile().await;
if let Some(name) = &cluster.setting_profile_name {
if let Some(profile) = get_profile_by_name(name).await? {
settings.merge(profile);
}
}
let state = State::get().await?;
let instance_path = &io::canonicalize(&cluster.path)?;
let mut metadata = state.metadata.write().await;
let versions = &metadata.get_vanilla_or_fetch().await?.versions;
let version_index = versions
.iter()
.position(|it| it.id == cluster.mc_version)
.ok_or_else(|| anyhow::anyhow!("invalid game version {}", cluster.mc_version))?;
let version = &versions[version_index];
let updated = version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0);
let version_jar_name = cluster
.mc_loader_version
.as_ref()
.map_or(version.id.clone(), |it| {
format!("{}-{}", version.id.clone(), it.clone())
});
let loader_version = metadata::get_loader_version(&cluster.mc_version, cluster.mc_loader, cluster.mc_loader_version.as_deref()).await?;
let version_info = metadata::download_version_info(
version,
loader_version.as_ref(),
None,
None,
)
.await?;
drop(metadata);
let java = java::get_recommended_java(&version_info, Some(&settings)).await?;
if java.is_none() {
return Err(JavaError::MissingJava.into());
}
todo!()
}

View file

@ -0,0 +1,27 @@
use std::sync::LazyLock;
use crate::store::credentials::MinecraftCredentials;
static WHOAMI: LazyLock<(String, String)> = LazyLock::new(|| {
(whoami::username(), whoami::realname())
});
#[must_use]
pub fn censor_line(
creds: &MinecraftCredentials,
mut output: String
) -> String {
let username = &WHOAMI.0;
let realname = &WHOAMI.1;
output = output.replace(&format!("/{username}/"), "/{ENV_USERNAME}/");
output = output.replace(&format!("\\{username}\\"), "\\{ENV_USERNAME}\\");
output = output.replace(&format!("/{realname}/"), "/{ENV_REALNAME}/");
output = output.replace(&format!("\\{realname}\\"), "\\{ENV_REALNAME}\\");
output = output.replace(&creds.access_token, "{MC_ACCESS_TOKEN}");
output = output.replace(&creds.username, "{MC_USERNAME}");
output = output.replace(&creds.id.as_simple().to_string(), "{MC_UUID}");
output = output.replace(&creds.id.as_hyphenated().to_string(), "{MC_UUID}");
output
}

View file

@ -1,4 +1,5 @@
pub mod metadata;
pub mod launch;
pub mod arguments;
pub mod rules;
pub mod launch;
pub mod log;
pub mod metadata;
pub mod rules;

View file

@ -1,7 +1,20 @@
use serde::Serialize;
use uuid::Uuid;
use crate::LauncherResult;
use crate::store::State;
use crate::store::ingress::{IngressId, IngressType, SubIngress};
#[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)]
pub struct IngressPayload {
pub id: Uuid,
pub message: String,
pub ingress_type: IngressType,
pub percent: Option<f64>,
pub total: f64,
}
#[tracing::instrument]
pub async fn init_ingress(
ingress_type: IngressType,

View file

@ -20,13 +20,13 @@ pub async fn get_latest_java() -> LauncherResult<Option<java_versions::Model>> {
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`]
pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
let major_version = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse::<i32>()
let major_version: u32 = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse()
} else {
let split = info.java_version.split_once('.').unwrap_or_default();
split.0.parse::<i32>()
split.0.parse()
}.or_else(
|_| info.java_version.parse::<i32>()
|_| info.java_version.parse()
).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?;
Ok(java_versions::ActiveModel {
@ -50,7 +50,7 @@ pub async fn get_java_all() -> LauncherResult<Vec<java_versions::Model>> {
}
/// Returns the specific Java version by ID
pub async fn get_java_by_id(id: i32) -> LauncherResult<Option<java_versions::Model>> {
pub async fn get_java_by_id(id: u64) -> LauncherResult<Option<java_versions::Model>> {
let db = &State::get().await?.db;
Ok(java_versions::Entity::find()
@ -60,7 +60,7 @@ pub async fn get_java_by_id(id: i32) -> LauncherResult<Option<java_versions::Mod
}
/// Returns all versions of Java for a given major version
pub async fn get_all_java_by_major(major: i32) -> LauncherResult<Vec<java_versions::Model>> {
pub async fn get_all_java_by_major(major: u32) -> LauncherResult<Vec<java_versions::Model>> {
let db = &State::get().await?.db;
Ok(java_versions::Entity::find()
@ -71,7 +71,7 @@ pub async fn get_all_java_by_major(major: i32) -> LauncherResult<Vec<java_versio
}
/// Returns the latest Java version for a given major version
pub async fn get_latest_java_by_major(major: i32) -> LauncherResult<Option<java_versions::Model>> {
pub async fn get_latest_java_by_major(major: u32) -> LauncherResult<Option<java_versions::Model>> {
let db = &State::get().await?.db;
Ok(java_versions::Entity::find()

View file

@ -18,6 +18,8 @@ pub enum JavaError {
ParseVersion(String, #[source] std::num::ParseIntError),
#[error("failed to execute java command")]
Execute(#[from] std::io::Error),
#[error("no java installations found")]
MissingJava,
}
/// Returns the relative path to the Java executable
@ -133,8 +135,43 @@ pub async fn get_recommended_java(
return Ok(None);
};
#[allow(clippy::cast_possible_wrap)]
dao::get_latest_java_by_major(supported_ver.major_version as i32).await
dao::get_latest_java_by_major(supported_ver.major_version).await
}
pub async fn prepare_java(
major: u32,
) -> LauncherResult<java_versions::Model> {
let id = init_ingress(
IngressType::JavaPrepare,
"preparing java",
100.0,
)
.await?;
let java = dao::get_latest_java_by_major(major).await?;
if let Some(java) = &java {
send_ingress(&id, 100.0).await?;
return Ok(java.clone());
}
let java = locate_java().await?;
if !java.is_empty() {
for (path, info) in java {
dao::insert_java(path, info).await?;
}
if let Some(java) = dao::get_latest_java_by_major(major).await? {
send_ingress(&id, 100.0).await?;
return Ok(java);
}
}
tracing::warn!("no java installations found on the system, please install java manually or use the launcher to download it");
// TODO: java runtime not found - should prob download
Err(JavaError::MissingJava.into())
}
/// Attempts to scan common paths for Java installations.

View file

@ -4,4 +4,5 @@ pub mod ingress;
pub mod game;
pub mod proxy;
pub mod java;
pub mod processes;
pub mod setting_profiles;

View file

@ -0,0 +1,18 @@
use serde::Serialize;
use crate::store::processes::Process;
#[derive(Debug, Serialize)]
pub enum ProcessPayload {
Started {
process: Process,
},
Stopped {
pid: u32,
exit_code: i32,
},
Output {
pid: u32,
output: String,
},
}

View file

@ -0,0 +1,27 @@
use serde::Serialize;
use crate::{error::LauncherResult, store::proxy::ProxyState};
#[onelauncher_macro::specta(with_event)]
#[derive(Debug, Serialize)]
pub enum LauncherEvent {
Ingress(crate::api::ingress::IngressPayload),
Message(crate::api::proxy::message::MessagePayload),
Process(crate::api::processes::ProcessPayload),
}
pub async fn send_event(event: LauncherEvent) {
let Ok(proxy) = ProxyState::get() else {
tracing::error!("failed to get proxy state");
return;
};
if let Err(e) = proxy.send_event(event).await {
tracing::error!("failed to send event: {e}");
}
}
pub async fn try_send_event(event: LauncherEvent) -> LauncherResult<()> {
let proxy = ProxyState::get()?;
proxy.send_event(event).await
}

View file

@ -0,0 +1,7 @@
#[cfg(feature = "tauri")] mod proxy_tauri;
#[cfg(feature = "cli")] mod proxy_cli;
mod proxy_empty;
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
pub use proxy_empty::ProxyEmpty;

View file

@ -0,0 +1,60 @@
use std::collections::HashMap;
use indicatif::ProgressBar;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult};
#[derive(Default, Debug)]
pub struct ProxyCli {
ingress_feeds: RwLock<HashMap<Uuid, ProgressBar>>,
}
impl ProxyCli {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait::async_trait]
impl LauncherProxy for ProxyCli {
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
match event {
LauncherEvent::Ingress(ingress) => {
let mut feeds = self.ingress_feeds.write().await;
let completed = ingress.percent.is_none();
if let Some(progress) = feeds.get(&ingress.id) {
if completed {
progress.finish();
} else {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let new_pos = (ingress.percent.unwrap() * CLI_TOTAL_INGRESS as f64).round() as u64;
progress.set_position(new_pos);
}
} else if !completed {
let progress = ProgressBar::new(CLI_TOTAL_INGRESS);
progress.set_message(ingress.message.clone());
progress.set_position(0);
progress.set_style(indicatif::ProgressStyle::default_bar().template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
).unwrap().progress_chars("#>-"));
feeds.insert(ingress.id, progress);
}
},
LauncherEvent::Message(message) => {
println!("[{:?}] {}", message.level, message.message);
},
_ => {
println!("{:#?}", event);
},
}
Ok(())
}
}

View file

@ -0,0 +1,48 @@
use crate::{api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult};
#[derive(Default, Debug)]
pub struct ProxyEmpty;
impl ProxyEmpty {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[async_trait::async_trait]
impl LauncherProxy for ProxyEmpty {
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
match event {
LauncherEvent::Ingress(ingress) => {
println!(
"{} [{}] {}",
ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
ingress.id,
ingress.message
);
},
LauncherEvent::Message(message) => {
println!("[{:?}] {}", message.level, message.message);
},
LauncherEvent::Process(payload) => {
println!("{payload:#?}");
},
}
Ok(())
}
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()> {
println!("hidden window");
Ok(())
}
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()> {
println!("shown window");
Ok(())
}
}

View file

@ -2,9 +2,7 @@ use tauri::{AppHandle, Emitter, Manager};
use tauri_specta::Event;
use tracing::{error, warn};
use crate::{store::ingress::IngressPayload, LauncherResult};
use super::{message::{MessageLevel, MessagePayload}, LauncherProxy};
use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
#[derive(Debug)]
pub struct ProxyTauri {
@ -22,21 +20,20 @@ impl ProxyTauri {
#[async_trait::async_trait]
impl LauncherProxy for ProxyTauri {
async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> {
self.handle
.emit(IngressPayload::NAME, ingress)
.map_err(Into::into)
}
async fn send_message(&self, message: MessagePayload) -> LauncherResult<()> {
match message.level {
MessageLevel::Info => {},
MessageLevel::Warning => warn!("{}", message.message),
MessageLevel::Error => error!("{}", message.message),
};
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
match event {
LauncherEvent::Message(message) => {
match message.level {
MessageLevel::Info => {},
MessageLevel::Warning => warn!("{}", message.message),
MessageLevel::Error => error!("{}", message.message),
};
},
_ => {}
}
self.handle
.emit(MessagePayload::NAME, message)
.emit_all(LauncherEvent::NAME, event)
.map_err(Into::into)
}

View file

@ -1,5 +1,7 @@
use serde::Serialize;
use super::event::send_event;
#[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)]
pub struct MessagePayload {
@ -11,48 +13,57 @@ pub struct MessagePayload {
#[derive(Serialize, Debug, Clone)]
pub enum MessageLevel {
Info,
Warning,
Warn,
Error,
}
pub fn send_message(level: MessageLevel, message: String) {
tokio::spawn(async move {
let payload = MessagePayload {
level,
message,
};
send_event(super::event::LauncherEvent::Message(payload)).await;
});
}
#[macro_export]
macro_rules! send_message {
($level:tt, $($message:tt)*) => {
$crate::api::proxy::message::send_message(
$level,
format!($($message)*),
)
};
}
#[macro_export]
macro_rules! send_info {
($($message:tt)*) => {
$crate::api::proxy::message::send_message(
$crate::api::proxy::message::MessageLevel::Info,
format!($($message)*),
)
};
}
#[macro_export]
macro_rules! send_warning {
($($message:tt)*) => {{
tokio::spawn(async move {
let _ = match $crate::store::proxy::ProxyState::get() {
Ok(proxy) => {
let payload = $crate::api::proxy::message::MessagePayload {
level: $crate::api::proxy::message::MessageLevel::Warning,
message: format!($($message)*),
};
let _ = proxy.send_message(payload).await;
},
Err(err) => {
tracing::warn!($($message)*);
tracing::warn!("failed to send warning: {}", err);
},
};
})
}};
($($message:tt)*) => {
$crate::api::proxy::message::send_message(
$crate::api::proxy::message::MessageLevel::Warn,
format!($($message)*),
)
};
}
#[macro_export]
macro_rules! send_error {
($($message:tt)*) => {{
tokio::spawn(async move {
let _ = match $crate::store::proxy::ProxyState::get() {
Ok(proxy) => {
let payload = $crate::api::proxy::message::MessagePayload {
level: $crate::api::proxy::message::MessageLevel::Error,
message: format!($($message)*),
};
let _ = proxy.send_message(payload).await;
},
Err(err) => {
tracing::error!($($message)*);
tracing::error!("failed to send error: {}", err);
},
};
})
}};
($($message:tt)*) => {
$crate::api::proxy::message::send_message(
$crate::api::proxy::message::MessageLevel::Error,
format!($($message)*),
)
};
}

View file

@ -1,22 +1,12 @@
mod r#impl;
pub mod message;
pub mod event;
#[cfg(feature = "tauri")] mod proxy_tauri;
#[cfg(feature = "cli")] mod proxy_cli;
mod proxy_empty;
use std::fmt::Debug;
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
pub use proxy_empty::ProxyEmpty;
#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli;
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;
pub use r#impl::*;
#[async_trait::async_trait]
pub trait LauncherProxy: Send + Sync + Debug {
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>;
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
pub trait LauncherProxy: Send + Sync + std::fmt::Debug {
async fn send_event(&self, event: event::LauncherEvent) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()>;
@ -24,3 +14,6 @@ pub trait LauncherProxy: Send + Sync + Debug {
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()>;
}
#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli;
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;

View file

@ -1,57 +0,0 @@
use std::collections::HashMap;
use indicatif::ProgressBar;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{constants::CLI_TOTAL_INGRESS, store::ingress::IngressPayload, LauncherResult};
use super::LauncherProxy;
#[derive(Default, Debug)]
pub struct ProxyCli {
ingress_feeds: RwLock<HashMap<Uuid, ProgressBar>>,
}
impl ProxyCli {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait::async_trait]
impl LauncherProxy for ProxyCli {
async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> {
let mut feeds = self.ingress_feeds.write().await;
let completed = ingress.percent.is_none();
if let Some(progress) = feeds.get(&ingress.id) {
if completed {
progress.finish();
} else {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let new_pos = (ingress.percent.unwrap() * CLI_TOTAL_INGRESS as f64).round() as u64;
progress.set_position(new_pos);
}
} else if !completed {
let progress = ProgressBar::new(CLI_TOTAL_INGRESS);
progress.set_message(ingress.message.clone());
progress.set_position(0);
progress.set_style(indicatif::ProgressStyle::default_bar().template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
).unwrap().progress_chars("#>-"));
feeds.insert(ingress.id, progress);
}
Ok(())
}
async fn send_message(&self, message: crate::api::proxy::message::MessagePayload) -> LauncherResult<()> {
println!("[{:?}] {}", message.level, message.message);
Ok(())
}
}

View file

@ -1,45 +0,0 @@
use crate::{store::ingress::IngressPayload, LauncherResult};
use super::LauncherProxy;
#[derive(Default, Debug)]
pub struct ProxyEmpty;
impl ProxyEmpty {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[async_trait::async_trait]
impl LauncherProxy for ProxyEmpty {
async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> {
println!(
"{} [{}] {}",
ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
ingress.id,
ingress.message
);
Ok(())
}
async fn send_message(&self, message: crate::api::proxy::message::MessagePayload) -> LauncherResult<()> {
println!("[{:?}] {}", message.level, message.message);
Ok(())
}
#[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()> {
println!("hidden window");
Ok(())
}
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()> {
println!("shown window");
Ok(())
}
}

View file

@ -1,60 +1,75 @@
use onelauncher_entity::setting_profiles;
use sea_orm::{ActiveModelTrait, EntityTrait};
use crate::{error::{DaoError, LauncherResult}, store::State};
use crate::store::State;
pub async fn get_all_profiles() -> LauncherResult<Vec<setting_profiles::Model>> {
let state = State::get().await?;
let db = &state.db;
pub mod dao {
use onelauncher_entity::setting_profiles;
use sea_orm::{ActiveModelTrait, EntityTrait};
Ok(setting_profiles::Entity::find()
.all(db)
.await?)
}
use crate::{error::{DaoError, LauncherResult}, store::State};
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
if let Some(name) = name {
let profile = get_profile_by_name(name).await?;
if let Some(profile) = profile {
return Ok(profile);
pub async fn get_all_profiles() -> LauncherResult<Vec<setting_profiles::Model>> {
let state = State::get().await?;
let db = &state.db;
Ok(setting_profiles::Entity::find()
.all(db)
.await?)
}
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
if let Some(name) = name {
let profile = get_profile_by_name(name).await?;
if let Some(profile) = profile {
return Ok(profile);
}
}
let state = State::get().await?;
let settings = &state.settings.read().await;
Ok(settings.global_game_settings.clone())
}
let state = State::get().await?;
let settings = &state.settings.read().await;
Ok(settings.global_game_settings.clone())
}
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
let state = State::get().await?;
let db = &state.db;
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
let state = State::get().await?;
let db = &state.db;
Ok(setting_profiles::Entity::find_by_id(name)
.one(db)
.await?)
}
Ok(setting_profiles::Entity::find_by_id(name)
.one(db)
.await?)
}
pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::Model> {
if let Some(name) = profile.name.try_as_ref() {
if name.is_empty() || profile.is_global() {
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::Model> {
if let Some(name) = profile.name.try_as_ref() {
if name.is_empty() || profile.is_global() {
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
}
}
let state = State::get().await?;
let db = &state.db;
Ok(profile.insert(db).await?)
}
let state = State::get().await?;
let db = &state.db;
pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
Ok(profile.insert(db).await?)
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?;
if deleted.rows_affected == 0 {
return Err(DaoError::NotFound.into());
}
Ok(())
}
}
pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
pub async fn get_global_profile() -> setting_profiles::Model {
let Ok(state) = State::get().await else {
return setting_profiles::Model::default_global_profile();
};
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?;
if deleted.rows_affected == 0 {
return Err(DaoError::NotFound.into());
}
Ok(())
}
let settings = state.settings.read().await;
settings.global_game_settings.clone()
}

View file

@ -19,6 +19,8 @@ pub enum LauncherError {
#[error(transparent)]
MinecraftAuthError(#[from] crate::store::credentials::MinecraftAuthError),
#[error(transparent)]
ProcessError(#[from] crate::store::processes::ProcessError),
#[error(transparent)]
DaoError(#[from] DaoError),
#[error(transparent)]

View file

@ -4,10 +4,11 @@ use serde::Serialize;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::api::proxy::event::try_send_event;
use crate::LauncherResult;
use crate::api::ingress::IngressPayload;
use super::State;
use super::proxy::ProxyState;
#[derive(Default)]
pub struct IngressProcessor {
@ -73,21 +74,20 @@ impl IngressProcessor {
let uuid = &id.0;
let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?;
let proxy = ProxyState::get()?;
ingress.current += increment;
if let Some(message) = message {
ingress.message = message;
}
proxy
.send_ingress(IngressPayload {
id: uuid.to_owned(),
message: ingress.message.clone(),
ingress_type: ingress.ingress_type.clone(),
percent: Some(ingress.current / ingress.total),
total: ingress.total,
})
let payload = IngressPayload {
id: uuid.to_owned(),
message: ingress.message.clone(),
ingress_type: ingress.ingress_type.clone(),
percent: Some(ingress.current / ingress.total),
total: ingress.total,
};
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.await?;
Ok(())
@ -113,20 +113,11 @@ pub struct Ingress {
pub last_sent: f64,
}
#[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)]
pub struct IngressPayload {
pub id: Uuid,
pub message: String,
pub ingress_type: IngressType,
pub percent: Option<f64>,
pub total: f64,
}
#[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)]
pub enum IngressType {
Download { file_name: String },
JavaPrepare,
JavaCheck,
JavaLocate,
MinecraftDownload,
@ -182,7 +173,8 @@ impl SubIngressExt for SubIngress<'_> {
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
where
F: FnOnce(f64) -> f64 {
F: FnOnce(f64) -> f64,
{
SubIngress::from_sub(self, total(self.total))
}
}
@ -201,7 +193,8 @@ impl SubIngressExt for Option<&SubIngress<'_>> {
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
where
F: FnOnce(f64) -> f64 {
F: FnOnce(f64) -> f64,
{
self.map(|sub| SubIngress::from_sub(sub, total(sub.total)))
}
}
@ -217,16 +210,15 @@ impl Drop for IngressId {
let ingress = processor.remove(ingress_uuid).await?;
let proxy = ProxyState::get()?;
let payload = IngressPayload {
id: ingress.id,
message: "Completed".into(),
ingress_type: ingress.ingress_type,
percent: None,
total: ingress.total,
};
proxy
.send_ingress(IngressPayload {
id: ingress.id,
message: "Completed".into(),
ingress_type: ingress.ingress_type,
percent: None,
total: ingress.total,
})
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.await?;
tracing::trace!(

View file

@ -4,7 +4,8 @@ pub mod ingress;
pub mod proxy;
pub mod semaphore;
pub mod metadata;
pub mod credentials;
pub mod credentials;
pub mod processes;
mod core_options;
mod dirs;
mod settings;

View file

@ -0,0 +1,276 @@
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use onelauncher_entity::setting_profiles;
use serde::Serialize;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::api::cluster;
use crate::api::game::log::censor_line;
use crate::api::processes::ProcessPayload;
use crate::api::proxy::event::{send_event, LauncherEvent};
use crate::error::{DaoError, LauncherResult};
use crate::send_error;
use crate::utils::io::IOError;
use super::credentials::MinecraftCredentials;
use super::State;
#[derive(Debug, thiserror::Error)]
pub enum ProcessError {
#[error("launch hook exited with non-zero code {0}")]
HookUnsuccessful(i32),
#[error("pid equaled nothing")]
NoPid,
}
#[derive(Default)]
pub struct ProcessStore {
processes: RwLock<HashMap<u32, Arc<RwLock<Process>>>>,
}
#[derive(Debug, Serialize)]
pub struct Process {
pub pid: u32,
pub started_at: DateTime<Utc>,
pub cluster_id: u64,
pub post_hook: Option<String>,
pub account_id: Uuid,
#[serde(skip)]
pub child_type: Arc<RwLock<ChildType>>,
#[serde(skip)]
process_loop: Option<JoinHandle<LauncherResult<i32>>>,
}
#[derive(Debug, Serialize)]
pub enum ChildType {
Owned(#[serde(skip)] Child),
}
impl Process {
#[must_use]
pub const fn is_running(&self) -> bool {
self.process_loop.is_some()
}
}
impl ChildType {
#[must_use]
pub const fn new(child: Child) -> Self {
Self::Owned(child)
}
#[must_use]
pub fn id(&self) -> u32 {
match self {
Self::Owned(child) => child.id().unwrap_or(0),
}
}
// Attempts to return its exit code, if it has exited.
pub fn try_wait(&mut self) -> LauncherResult<Option<i32>> {
Ok(match self {
Self::Owned(child) => child
.try_wait()
.map_err(IOError::from)?
.map(|s| s.code().unwrap_or_default()),
})
}
// async blocking to wait for the process to finish and return exit code
pub async fn wait(&mut self) -> LauncherResult<i32> {
Ok(match self {
Self::Owned(child) => child
.wait()
.await
.map(|s| s.code().unwrap_or_default())
.map_err(IOError::from)?,
})
}
pub async fn kill(&mut self) -> LauncherResult<()> {
match self {
Self::Owned(child) => child.kill().await.map_err(IOError::from)?,
}
Ok(())
}
}
impl ProcessStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub async fn spawn(
&self,
cluster_id: u64,
creds: MinecraftCredentials,
settings: &setting_profiles::Model,
mut command: Command,
) -> LauncherResult<Arc<RwLock<Process>>> {
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
let mut child = command.spawn().map_err(IOError::from)?;
let started_at = Utc::now();
let Some(pid) = child.id() else {
return Err(ProcessError::NoPid.into());
};
let stdout = child.stdout.take().expect("couldn't get stdout of child");
let stderr = child.stderr.take().expect("couldn't get stderr of child");
#[cfg(target_os = "linux")]
{
if settings.os_extra.as_ref().is_some_and(|s| s.enable_gamemode.is_some_and(|x| x)) {
if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) {
tracing::warn!("failed to enable gamemode, continuing: {}", err);
}
}
}
let account_id = creds.id;
tokio::spawn(async move {
let mut stdout = BufReader::new(stdout).lines();
let mut stderr = BufReader::new(stderr).lines();
while let Ok(Some(line)) = tokio::select! {
line = stdout.next_line() => line,
line = stderr.next_line() => line,
} {
let censored = censor_line(&creds, line);
send_event(LauncherEvent::Process(ProcessPayload::Output {
pid,
output: censored,
})).await;
}
});
let child_type = Arc::new(RwLock::new(ChildType::new(child)));
let process = Process {
pid,
started_at,
cluster_id,
post_hook: settings.hook_post.clone(),
account_id,
child_type: child_type.clone(),
process_loop: Some(tokio::task::spawn(Self::process_loop(
cluster_id, settings.hook_post.clone(), child_type,
))),
};
let process = Arc::new(RwLock::new(process));
let mut write_guard = self.processes.write().await;
write_guard.insert(pid, process.clone());
tracing::info!("spawned process with pid {}", pid);
Ok(process)
}
pub async fn get(&self, pid: u32) -> Option<Arc<RwLock<Process>>> {
let read_guard = self.processes.read().await;
read_guard.get(&pid).cloned()
}
async fn process_loop(
cluster_id: u64,
post_hook: Option<String>,
child: Arc<RwLock<ChildType>>,
) -> LauncherResult<i32> {
let pid = child.read().await.id();
let mut last_updated = Utc::now();
let exit_code = loop {
let mut child = child.write().await;
if let Some(status) = child.try_wait()? {
tracing::info!("process exited with status: {:?}", status);
break status;
}
// Every 60 seconds, update the playtime of the process
let duration = Utc::now().signed_duration_since(last_updated).num_seconds();
if duration > 60 {
last_updated = Utc::now();
if let Err(err) = cluster::update_playtime(cluster_id, duration).await {
tracing::error!("failed to update playtime: {}", err);
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
};
let state = State::get().await?;
let store = &state.processes;
let mut write_guard = store.processes.write().await;
write_guard.remove(&pid);
tracing::debug!("removed process with pid {}", pid);
if let Some(post_hook) = post_hook {
if !post_hook.is_empty() {
let result: LauncherResult<()> = {
let cluster = cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or(DaoError::NotFound)?;
if let Err(err) = run_hook(&post_hook, &cluster.path).await {
send_error!("{err:?}");
}
Ok(())
};
if let Err(err) = result {
send_error!("failed to run post hook: {}", err);
}
}
}
send_event(LauncherEvent::Process(ProcessPayload::Stopped { pid, exit_code })).await;
Ok(exit_code)
}
}
pub async fn run_hook(hook: &str, cwd: &str) -> LauncherResult<Option<i32>> {
let mut split = hook.split(' ');
let Some(command) = split.next() else {
return Ok(None);
};
let result = Command::new(command)
.args(split)
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(IOError::from)?
.wait()
.await
.map_err(IOError::from)?;
let exit_code = result
.code()
.unwrap_or_else(|| i32::from(!result.success()));
if result.success() {
Ok(Some(exit_code))
} else {
Err(ProcessError::HookUnsuccessful(exit_code).into())
}
}

View file

@ -6,6 +6,7 @@ use tokio::sync::{OnceCell, RwLock};
use crate::LauncherResult;
use super::credentials::CredentialsStore;
use super::processes::ProcessStore;
use super::Settings;
use super::discord::DiscordRPC;
use super::ingress::IngressProcessor;
@ -20,6 +21,7 @@ pub struct State {
pub db: DatabaseConnection,
pub credentials: RwLock<CredentialsStore>,
pub metadata: RwLock<Metadata>,
pub processes: ProcessStore,
pub rpc: Option<DiscordRPC>,
}
@ -44,6 +46,7 @@ impl State {
let db = super::db::create_pool().await?;
let credentials = CredentialsStore::initialize().await?;
let metadata = Metadata::new();
let processes = ProcessStore::new();
let rpc = match DiscordRPC::initialize() {
Ok(rpc) => Some(rpc),
Err(err) => {
@ -58,6 +61,7 @@ impl State {
db,
credentials: RwLock::new(credentials),
metadata: RwLock::new(metadata),
processes,
rpc,
}))
}

View file

@ -27,4 +27,5 @@ chrono = { workspace = true }
uuid = { workspace = true }
url = { workspace = true }
interpulse = { workspace = true }
merge = { workspace = true }
specta = { workspace = true, optional = true }

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
@ -11,4 +13,21 @@ pub enum ClusterStage {
Downloading = 1,
Repairing = 2,
Ready = 3,
}
}
impl ClusterStage {
pub fn is_downloading(&self) -> bool {
matches!(self, Self::Downloading | Self::Repairing)
}
}
impl Display for ClusterStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
ClusterStage::NotReady => "Not Ready",
ClusterStage::Downloading => "Downloading",
ClusterStage::Repairing => "Repairing",
ClusterStage::Ready => "Ready",
})
}
}

View file

@ -1,3 +1,4 @@
pub mod cluster_stage;
pub mod icon;
pub mod loader;
pub mod loader;
pub mod settings;

View file

@ -0,0 +1,84 @@
use sea_orm::{sea_query::{self, Nullable, ValueType}, TryGetableFromJson};
use serde::{Deserialize, Serialize};
cfg_match! {
target_os = "windows" => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
}
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
}
}
}
}
target_os = "macos" => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
}
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
}
}
}
}
not(any(target_os = "windows", target_os = "macos")) => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsOsExtra {
pub enable_gamemode: Option<bool>,
}
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
enable_gamemode: Some(true),
}
}
}
}
}
impl From<SettingsOsExtra> for sea_query::Value {
fn from(value: SettingsOsExtra) -> Self {
sea_query::Value::Json(serde_json::to_value(value).ok().map(Box::new))
}
}
impl TryGetableFromJson for SettingsOsExtra {}
impl ValueType for SettingsOsExtra {
fn try_from(v: sea_orm::Value) -> Result<Self, sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => Ok(
serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
),
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
"SettingsOsExtra".to_string()
}
fn array_type() -> sea_query::ArrayType {
sea_query::ArrayType::Json
}
fn column_type() -> sea_orm::ColumnType {
sea_orm::ColumnType::Json
}
}
impl Nullable for SettingsOsExtra {
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub id: u64,
#[sea_orm(column_type = "Text")]
pub name: String,
}

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub cluster_id: i32,
pub cluster_id: u64,
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
pub package_hash: String,
}

View file

@ -8,13 +8,12 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub id: u64,
#[sea_orm(column_type = "Text")]
pub path: String,
pub stage: ClusterStage,
pub created_at: i32,
pub updated_at: i32,
pub group_id: Option<i32>,
pub created_at: i64,
pub group_id: Option<u64>,
#[sea_orm(column_type = "Text")]
pub name: String,
#[sea_orm(column_type = "Text")]
@ -22,15 +21,15 @@ pub struct Model {
pub mc_loader: GameLoader,
#[sea_orm(column_type = "Text", nullable)]
pub mc_loader_version: Option<String>,
pub last_played: Option<i32>,
pub overall_played: Option<i32>,
pub last_played: Option<i64>,
pub overall_played: Option<i64>,
#[sea_orm(column_type = "Text", nullable)]
pub icon_url: Option<Icon>,
#[sea_orm(column_type = "Text", nullable)]
pub setting_profile_name: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub linked_pack_id: Option<String>,
pub linked_pack_version: Option<i32>,
pub linked_pack_version: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub major_version: i32,
pub id: u64,
pub major_version: u32,
#[sea_orm(column_type = "Text")]
pub vendor_name: String,
#[sea_orm(column_type = "Text")]

View file

@ -17,8 +17,8 @@ pub struct Model {
pub display_name: String,
#[sea_orm(column_type = "Text")]
pub display_version: String,
pub type_id: i32,
pub provider_id: i32,
pub type_id: u32,
pub provider_id: u32,
#[sea_orm(column_type = "Text")]
pub provider_version: String,
#[sea_orm(column_type = "Text")]

View file

@ -1,8 +1,26 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
pub use super::cluster_groups::Entity as ClusterGroups;
pub use super::cluster_packages::Entity as ClusterPackages;
pub use super::clusters::Entity as Clusters;
pub use super::java_versions::Entity as JavaVersions;
pub use super::packages::Entity as Packages;
pub use super::setting_profiles::Entity as SettingProfiles;
pub mod entity {
pub use crate::cluster_groups::Entity as ClusterGroups;
pub use crate::cluster_packages::Entity as ClusterPackages;
pub use crate::clusters::Entity as Clusters;
pub use crate::java_versions::Entity as JavaVersions;
pub use crate::packages::Entity as Packages;
pub use crate::setting_profiles::Entity as SettingProfiles;
}
pub mod model {
pub use crate::cluster_groups::Model as ClusterGroup;
pub use crate::cluster_packages::Model as ClusterPackage;
pub use crate::clusters::Model as Cluster;
pub use crate::java_versions::Model as JavaVersion;
pub use crate::packages::Model as Package;
pub use crate::setting_profiles::Model as SettingsProfile;
pub use crate::cluster_groups::ActiveModel as ClusterGroupPartial;
pub use crate::cluster_packages::ActiveModel as ClusterPackagePartial;
pub use crate::clusters::ActiveModel as ClusterPartial;
pub use crate::java_versions::ActiveModel as JavaVersionPartial;
pub use crate::packages::ActiveModel as PackagePartial;
pub use crate::setting_profiles::ActiveModel as SettingsProfilePartial;
}

View file

@ -1,19 +1,24 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
use merge::Merge;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
use crate::settings::SettingsOsExtra;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, Merge)]
#[sea_orm(table_name = "setting_profiles")]
#[onelauncher_macro::specta]
#[merge(strategy = merge::option::overwrite_none)]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
#[merge(skip)]
pub name: String,
pub java_id: Option<i32>,
pub res_w: Option<i32>,
pub res_h: Option<i32>,
pub java_id: Option<u64>,
pub res_w: Option<u32>,
pub res_h: Option<u32>,
pub force_fullscreen: Option<bool>,
pub mem_max: Option<i32>,
pub mem_max: Option<u32>,
#[sea_orm(column_type = "Text", nullable)]
pub launch_args: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
@ -24,6 +29,8 @@ pub struct Model {
pub hook_wrapper: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub hook_post: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub os_extra: Option<SettingsOsExtra>
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -80,6 +87,7 @@ impl Model {
mem_max: Some(3072),
res_h: None,
res_w: None,
os_extra: Some(SettingsOsExtra::default())
}
}
}

View file

@ -1,3 +1,5 @@
#![feature(cfg_match)]
mod entities;
mod domain;

View file

@ -3,11 +3,20 @@ mod pin;
/// This macro is used to derive the `specta::Type` trait for the annotated item.
/// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
/// <br>
/// Expands to:
///
/// #### Expands to:
/// ```
/// #[cfg_attr(feature = "specta", derive(specta::Type))]
/// ```
///
/// ## `with_event` argument
/// Add `#[specta(with_event)]` to add the `tauri_specta::Event` derive.
///
/// #### Expands to:
/// ```
/// #[cfg_attr(feature = "specta", derive(specta::Type))]
/// #[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
/// ```
#[proc_macro_attribute]
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
specta::specta(attr, item)

View file

@ -42,6 +42,7 @@ create table setting_profiles (
hook_pre text,
hook_wrapper text,
hook_post text,
os_extra json_text,
constraint setting_profiles_pk primary key (name),
constraint setting_profiles_java_versions_id_fk foreign key (java_id) references java_versions(id)
);
@ -51,7 +52,6 @@ create table clusters (
path text not null,
stage integer default (0) not null,
created_at integer default (unixepoch()) not null,
updated_at integer default (unixepoch()) not null,
group_id integer,
name text not null,
mc_version text not null,
@ -62,7 +62,7 @@ create table clusters (
icon_url text,
setting_profile_name text,
linked_pack_id text,
linked_pack_version integer,
linked_pack_version text,
constraint clusters_pk primary key (id autoincrement),
constraint clusters_cluster_groups_id_fk foreign key (group_id) references cluster_groups(id),
constraint clusters_setting_profiles_name_fk foreign key (setting_profile_name) references setting_profiles(name)