forked from akanyan/STARTLINER
feat: etc
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
rust/**/*
|
@ -64,7 +64,8 @@ Arbitrary scripts are not supported by design and that will probably never chang
|
|||||||
- CHUNITHM support
|
- CHUNITHM support
|
||||||
- segatools as a special package
|
- segatools as a special package
|
||||||
- Progress bars and other GUI sugar
|
- Progress bars and other GUI sugar
|
||||||
- Rebuilding the profile only when necessary
|
- Search bar
|
||||||
|
- Start check
|
||||||
|
|
||||||
### Endgame
|
### Endgame
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
|
"@tauri-apps/plugin-fs": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "~2",
|
"@tauri-apps/plugin-shell": "~2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@ -4341,6 +4341,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
|
@ -39,6 +39,7 @@ async-std = "1.13.0"
|
|||||||
closure = "0.3.0"
|
closure = "0.3.0"
|
||||||
derive_more = { version = "2.0.1", features = ["display"] }
|
derive_more = { version = "2.0.1", features = ["display"] }
|
||||||
junction = "1.2.0"
|
junction = "1.2.0"
|
||||||
|
tauri-plugin-fs = "2"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
"permissions": [
|
||||||
],
|
"core:default",
|
||||||
"permissions": [
|
"opener:default",
|
||||||
"core:default",
|
"core:window:allow-close",
|
||||||
"opener:default",
|
"core:app:allow-app-hide",
|
||||||
"core:window:allow-close",
|
"shell:default",
|
||||||
"core:app:allow-app-hide",
|
"dialog:default",
|
||||||
"shell:default",
|
"deep-link:default",
|
||||||
"shell:default",
|
"fs:default",
|
||||||
"opener:default",
|
"fs:allow-data-read-recursive",
|
||||||
"dialog:default",
|
"fs:allow-data-write-recursive"
|
||||||
"dialog:default",
|
]
|
||||||
"deep-link:default"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
use crate::pkg::PkgKey;
|
use crate::pkg::PkgKey;
|
||||||
use crate::Profile;
|
|
||||||
use crate::pkg_store::PackageStore;
|
use crate::pkg_store::PackageStore;
|
||||||
|
use crate::Profile;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub profile: Option<Profile>,
|
pub profile: Option<Profile>,
|
||||||
pub pkgs: PackageStore
|
pub pkgs: PackageStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
@ -37,4 +39,14 @@ impl AppData {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
for pkg in &p.mods {
|
||||||
|
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
||||||
|
pkg.hash(&mut hasher);
|
||||||
|
x.version.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
hasher.finish().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,16 +20,24 @@ pub async fn startline(app: AppHandle) -> Result<(), String> {
|
|||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
|
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
// TODO if p.needsUpdate
|
let hash = appd.sum_packages(p);
|
||||||
liner::line_up(p).await.expect("Line-up failed");
|
liner::line_up(p, hash).await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
start::start(p, app_copy)
|
start::start(p, app_copy)
|
||||||
.map_err(|e| { log::error!("Error launching: {}", e.to_string()); e.to_string() })
|
.map_err(|e| e.to_string())
|
||||||
//Ok(())
|
|
||||||
} else {
|
} else {
|
||||||
Err("No profile".to_owned())
|
Err("No profile".to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn kill() -> Result<(), String> {
|
||||||
|
start::pkill("amdaemon.exe").await;
|
||||||
|
// The start routine will kill the other process
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
||||||
log::debug!("invoke: install_package({})", key);
|
log::debug!("invoke: install_package({})", key);
|
||||||
|
@ -107,7 +107,8 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::init_profile,
|
cmd::init_profile,
|
||||||
cmd::save_profile,
|
cmd::save_profile,
|
||||||
cmd::startline,
|
cmd::startline,
|
||||||
cmd::set_cfg
|
cmd::kill,
|
||||||
|
cmd::set_cfg,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -17,9 +17,23 @@ async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Resul
|
|||||||
junction::create(src, dst)
|
junction::create(src, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn line_up(p: &Profile) -> Result<()> {
|
pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
||||||
let dir_out = util::profile_dir(&p);
|
let hash_path = p.dir().join(".sl-state");
|
||||||
log::info!("Preparing {}", dir_out.to_string_lossy());
|
let prev_hash = fs::read_to_string(&hash_path).await.unwrap_or_default();
|
||||||
|
if prev_hash != pkg_hash {
|
||||||
|
log::debug!("state {} -> {}", prev_hash, pkg_hash);
|
||||||
|
fs::write(hash_path, pkg_hash).await
|
||||||
|
.map_err(|e| anyhow!("Unable to write the state file: {}", e))?;
|
||||||
|
prepare_packages(p).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_config(p).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_packages(p: &Profile) -> Result<()> {
|
||||||
|
let dir_out = p.dir();
|
||||||
|
|
||||||
let mut futures = JoinSet::new();
|
let mut futures = JoinSet::new();
|
||||||
if dir_out.join("BepInEx").exists() {
|
if dir_out.join("BepInEx").exists() {
|
||||||
@ -34,25 +48,29 @@ pub async fn line_up(p: &Profile) -> Result<()> {
|
|||||||
|
|
||||||
for m in &p.mods {
|
for m in &p.mods {
|
||||||
log::debug!("Preparing {}", m);
|
log::debug!("Preparing {}", m);
|
||||||
let dir_out = util::profile_dir(&p);
|
|
||||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||||
let bpx = util::pkg_dir_of(namespace, &name[1..])
|
let bpx_dir = util::pkg_dir_of(namespace, &name[1..])
|
||||||
.join("app")
|
.join("app")
|
||||||
.join("BepInEx");
|
.join("BepInEx");
|
||||||
if bpx.exists() {
|
if bpx_dir.exists() {
|
||||||
util::copy_recursive(&bpx, &dir_out.join("BepInEx"))?;
|
util::copy_recursive(&bpx_dir, &dir_out.join("BepInEx"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let opt = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
||||||
if opt.exists() {
|
if opt_dir.exists() {
|
||||||
let x = opt.read_dir().unwrap().next().unwrap()?;
|
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
|
||||||
if x.metadata()?.is_dir() {
|
if x.metadata()?.is_dir() {
|
||||||
symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
|
symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo temporary
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_config(p: &Profile) -> Result<()> {
|
||||||
|
let dir_out = p.dir();
|
||||||
|
|
||||||
let ini_in_raw = fs::read_to_string(p.exe_dir.join("segatools.ini")).await?;
|
let ini_in_raw = fs::read_to_string(p.exe_dir.join("segatools.ini")).await?;
|
||||||
let ini_in = Ini::load_from_str(&ini_in_raw)?;
|
let ini_in = Ini::load_from_str(&ini_in_raw)?;
|
||||||
let mut opt_dir_in = PathBuf::from(
|
let mut opt_dir_in = PathBuf::from(
|
||||||
@ -78,7 +96,7 @@ pub async fn line_up(p: &Profile) -> Result<()> {
|
|||||||
util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))?
|
util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))?
|
||||||
);
|
);
|
||||||
|
|
||||||
if prepare_aime(p).await.unwrap_or(false) {
|
if p.get_bool("aime", false) {
|
||||||
ini_out.with_section(Some("aime"))
|
ini_out.with_section(Some("aime"))
|
||||||
.set("enable", "1")
|
.set("enable", "1")
|
||||||
.set("aimePath", util::path_to_str(dir_out.join("aime.txt"))?);
|
.set("aimePath", util::path_to_str(dir_out.join("aime.txt"))?);
|
||||||
@ -93,16 +111,4 @@ pub async fn line_up(p: &Profile) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
// Todo multiple codes
|
|
||||||
async fn prepare_aime(p: &Profile) -> Result<bool> {
|
|
||||||
if p.get_bool("aime", true) {
|
|
||||||
if let Some(code) = p.cfg.get("aime-code") {
|
|
||||||
let code = code.as_str().expect("Invalid config");
|
|
||||||
fs::write(util::profile_dir(&p).join("aime.txt"), code).await?;
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ use tokio::fs;
|
|||||||
use crate::{model::{local, rainy}, util};
|
use crate::{model::{local, rainy}, util};
|
||||||
|
|
||||||
// {namespace}-{name}
|
// {namespace}-{name}
|
||||||
#[derive(Eq, Hash, PartialEq, Clone, Serialize, Deserialize, Display)]
|
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
||||||
pub struct PkgKey(pub String);
|
pub struct PkgKey(pub String);
|
||||||
|
|
||||||
// {namespace}-{name}-{version}
|
// {namespace}-{name}-{version}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::{collections::{HashMap, HashSet}, path::PathBuf};
|
use anyhow::Result;
|
||||||
|
use std::{collections::{BTreeSet, HashMap}, path::PathBuf};
|
||||||
use crate::{model::misc, pkg::PkgKey, util};
|
use crate::{model::misc, pkg::PkgKey, util};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@ -11,7 +12,7 @@ pub struct Profile {
|
|||||||
pub game: misc::Game,
|
pub game: misc::Game,
|
||||||
pub exe_dir: PathBuf,
|
pub exe_dir: PathBuf,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mods: HashSet<PkgKey>,
|
pub mods: BTreeSet<PkgKey>,
|
||||||
pub wine_runtime: Option<PathBuf>,
|
pub wine_runtime: Option<PathBuf>,
|
||||||
pub wine_prefix: Option<PathBuf>,
|
pub wine_prefix: Option<PathBuf>,
|
||||||
// cfg is temporarily just a map to make iteration easier
|
// cfg is temporarily just a map to make iteration easier
|
||||||
@ -25,7 +26,7 @@ impl Profile {
|
|||||||
game: misc::Game::Ongeki,
|
game: misc::Game::Ongeki,
|
||||||
exe_dir: exe_path.parent().unwrap().to_owned(),
|
exe_dir: exe_path.parent().unwrap().to_owned(),
|
||||||
name: "ongeki-default".to_owned(),
|
name: "ongeki-default".to_owned(),
|
||||||
mods: HashSet::new(),
|
mods: BTreeSet::new(),
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()),
|
wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()),
|
||||||
@ -45,6 +46,13 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dir(&self) -> PathBuf {
|
||||||
|
util::get_dirs()
|
||||||
|
.data_dir()
|
||||||
|
.join("profile-".to_owned() + &self.name)
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load() -> Option<Profile> {
|
pub fn load() -> Option<Profile> {
|
||||||
let path = util::get_dirs()
|
let path = util::get_dirs()
|
||||||
.config_dir()
|
.config_dir()
|
||||||
@ -65,6 +73,11 @@ impl Profile {
|
|||||||
log::info!("Written to {}", path.to_string_lossy());
|
log::info!("Written to {}", path.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> {
|
||||||
|
self.cfg.get(key)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
||||||
self.cfg.get(key)
|
self.cfg.get(key)
|
||||||
.and_then(|c| c.as_bool())
|
.and_then(|c| c.as_bool())
|
||||||
|
@ -1,50 +1,40 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::fs::File;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use std::process::Stdio;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|
||||||
let p = p.clone();
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let rv = Command::new(p.wine_runtime.as_ref().unwrap())
|
|
||||||
.env(
|
|
||||||
"SEGATOOLS_CONFIG_PATH",
|
|
||||||
util::profile_dir(&p).join("segatools.ini"),
|
|
||||||
)
|
|
||||||
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
|
|
||||||
.arg(p.exe_dir.join("start.bat"))
|
|
||||||
.spawn();
|
|
||||||
match rv {
|
|
||||||
Ok(mut child) => {
|
|
||||||
_ = child.wait().await;
|
|
||||||
log::debug!("Fin");
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Fail: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = app.emit("launch-end", "");
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
static CREATE_NO_WINDOW: i32 = 0x08000000;
|
||||||
|
|
||||||
pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
let create_no_window = 0x08000000;
|
let ini_path = p.dir().join("segatools.ini");
|
||||||
|
|
||||||
let ini_path = util::profile_dir(&p).join("segatools.ini");
|
|
||||||
|
|
||||||
log::debug!("With path {}", ini_path.to_string_lossy());
|
log::debug!("With path {}", ini_path.to_string_lossy());
|
||||||
log::info!("Launching amdaemon");
|
|
||||||
|
|
||||||
let mut amd_builder = Command::new("cmd.exe");
|
let mut game_builder;
|
||||||
let mut game_builder = Command::new(p.exe_dir.join("inject.exe"));
|
let mut amd_builder;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
game_builder = Command::new(p.exe_dir.join("inject.exe"));
|
||||||
|
amd_builder = Command::new("cmd.exe");
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let wine = p.wine_runtime.as_ref()
|
||||||
|
.expect("No wine path specified");
|
||||||
|
|
||||||
|
game_builder = Command::new(wine);
|
||||||
|
amd_builder = Command::new(wine);
|
||||||
|
|
||||||
|
game_builder.arg(p.exe_dir.join("inject.exe"));
|
||||||
|
amd_builder.arg("cmd.exe");
|
||||||
|
}
|
||||||
|
|
||||||
let display_mode = p.get_str("display-mode", "borderless");
|
let display_mode = p.get_str("display-mode", "borderless");
|
||||||
|
|
||||||
@ -76,24 +66,38 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
game_builder.arg("-popupwindow");
|
game_builder.arg("-popupwindow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg!(debug_assertions) {
|
#[cfg(target_os = "linux")]
|
||||||
amd_builder
|
{
|
||||||
.creation_flags(create_no_window)
|
let wineprefix = p.wine_prefix.as_ref()
|
||||||
// Obviously, this is a meme
|
.expect("No wineprefix specified");
|
||||||
// Output will be handled properly at a later time
|
amd_builder.env("WINEPREFIX", wineprefix);
|
||||||
.stdout(Stdio::null())
|
game_builder.env("WINEPREFIX", wineprefix);
|
||||||
.stderr(Stdio::null());
|
}
|
||||||
|
|
||||||
game_builder
|
|
||||||
.creation_flags(create_no_window)
|
let amd_log = File::create(p.dir().join("amdaemon.log"))?;
|
||||||
.stdout(Stdio::null())
|
let game_log = File::create(p.dir().join("mu3.log"))?;
|
||||||
.stderr(Stdio::null());
|
|
||||||
|
amd_builder
|
||||||
|
.stdout(Stdio::from(amd_log));
|
||||||
|
// do they use stderr?
|
||||||
|
|
||||||
|
game_builder
|
||||||
|
.stdout(Stdio::from(game_log));
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
amd_builder.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
game_builder.creation_flags(CREATE_NO_WINDOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.get_bool("intel", false) == true {
|
if p.get_bool("intel", false) == true {
|
||||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::info!("Launching amdaemon: {:?}", amd_builder);
|
||||||
|
log::info!("Launching mu3: {:?}", game_builder);
|
||||||
|
|
||||||
let mut amd = amd_builder.spawn()?;
|
let mut amd = amd_builder.spawn()?;
|
||||||
let mut game = game_builder.spawn()?;
|
let mut game = game_builder.spawn()?;
|
||||||
|
|
||||||
@ -101,19 +105,24 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
let mut set = JoinSet::new();
|
let mut set = JoinSet::new();
|
||||||
|
|
||||||
set.spawn(async move {
|
set.spawn(async move {
|
||||||
amd.wait().await.expect("amdaemon failed to run")
|
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon.exe")
|
||||||
});
|
});
|
||||||
|
|
||||||
set.spawn(async move {
|
set.spawn(async move {
|
||||||
game.wait().await.expect("mu3 failed to run")
|
(game.wait().await.expect("mu3 failed to run"), "mu3.exe")
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = set.join_next().await.expect("No spawn").expect("No result");
|
_ = app.emit("launch-start", "");
|
||||||
|
|
||||||
log::info!("One of the processes died with return code {}", res);
|
let (rc, process_name) = set.join_next().await.expect("No spawn").expect("No result");
|
||||||
|
|
||||||
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("amdaemon.exe").creation_flags(create_no_window).output().await;
|
log::info!("{} died with return code {}", process_name, rc);
|
||||||
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").creation_flags(create_no_window).output().await;
|
|
||||||
|
if process_name == "amdaemon.exe" {
|
||||||
|
pkill("mu3.exe").await;
|
||||||
|
} else {
|
||||||
|
pkill("amdaemon.exe").await;
|
||||||
|
}
|
||||||
|
|
||||||
set.join_next().await.expect("No spawn").expect("No result");
|
set.join_next().await.expect("No spawn").expect("No result");
|
||||||
|
|
||||||
@ -123,4 +132,16 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn pkill(process_name: &str) {
|
||||||
|
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name)
|
||||||
|
.creation_flags(CREATE_NO_WINDOW).output().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn pkill(process_name: &str) {
|
||||||
|
_ = Command::new("pkill").arg(process_name)
|
||||||
|
.output().await;
|
||||||
}
|
}
|
@ -2,8 +2,6 @@ use anyhow::{anyhow, Result};
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::profile::Profile;
|
|
||||||
|
|
||||||
pub fn get_dirs() -> ProjectDirs {
|
pub fn get_dirs() -> ProjectDirs {
|
||||||
ProjectDirs::from("org", "7EVENDAYSHOLIDAYS", "STARTLINER")
|
ProjectDirs::from("org", "7EVENDAYSHOLIDAYS", "STARTLINER")
|
||||||
.expect("Unable to set up config directories")
|
.expect("Unable to set up config directories")
|
||||||
@ -21,13 +19,6 @@ pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf {
|
|||||||
pkg_dir().join(format!("{}-{}", namespace, name)).to_owned()
|
pkg_dir().join(format!("{}-{}", namespace, name)).to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile_dir(p: &Profile) -> PathBuf {
|
|
||||||
get_dirs()
|
|
||||||
.data_dir()
|
|
||||||
.join("profile-".to_owned() + &p.name)
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_dir() -> PathBuf {
|
pub fn cache_dir() -> PathBuf {
|
||||||
get_dirs().cache_dir().to_owned()
|
get_dirs().cache_dir().to_owned()
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,12 @@ import TabList from 'primevue/tablist';
|
|||||||
import TabPanel from 'primevue/tabpanel';
|
import TabPanel from 'primevue/tabpanel';
|
||||||
import TabPanels from 'primevue/tabpanels';
|
import TabPanels from 'primevue/tabpanels';
|
||||||
import Tabs from 'primevue/tabs';
|
import Tabs from 'primevue/tabs';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import ModList from './ModList.vue';
|
import ModList from './ModList.vue';
|
||||||
import ModStore from './ModStore.vue';
|
import ModStore from './ModStore.vue';
|
||||||
import Options from './Options.vue';
|
import Options from './Options.vue';
|
||||||
|
import StartButton from './StartButton.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
import { changePrimaryColor } from '../util';
|
import { changePrimaryColor } from '../util';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ const store = usePkgStore();
|
|||||||
store.setupListeners();
|
store.setupListeners();
|
||||||
|
|
||||||
const currentTab = ref('3');
|
const currentTab = ref('3');
|
||||||
const startEnabled = ref(false);
|
|
||||||
|
|
||||||
const loadProfile = async (openWindow: boolean) => {
|
const loadProfile = async (openWindow: boolean) => {
|
||||||
await store.reloadProfile();
|
await store.reloadProfile();
|
||||||
@ -42,7 +40,6 @@ const loadProfile = async (openWindow: boolean) => {
|
|||||||
}
|
}
|
||||||
if (store.profile !== null) {
|
if (store.profile !== null) {
|
||||||
changePrimaryColor(store.profile.game);
|
changePrimaryColor(store.profile.game);
|
||||||
startEnabled.value = true;
|
|
||||||
currentTab.value = '0';
|
currentTab.value = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,11 +48,6 @@ const loadProfile = async (openWindow: boolean) => {
|
|||||||
|
|
||||||
const isProfileDisabled = computed(() => store.profile === null);
|
const isProfileDisabled = computed(() => store.profile === null);
|
||||||
|
|
||||||
const startline = async () => {
|
|
||||||
startEnabled.value = false;
|
|
||||||
await invoke('startline');
|
|
||||||
};
|
|
||||||
|
|
||||||
onOpenUrl((urls) => {
|
onOpenUrl((urls) => {
|
||||||
console.log('deep link:', urls);
|
console.log('deep link:', urls);
|
||||||
});
|
});
|
||||||
@ -63,10 +55,6 @@ onOpenUrl((urls) => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProfile(false);
|
await loadProfile(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
listen('launch-end', () => {
|
|
||||||
startEnabled.value = true;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -87,15 +75,7 @@ listen('launch-end', () => {
|
|||||||
><div class="pi pi-question-circle"></div
|
><div class="pi pi-question-circle"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<Button
|
<StartButton />
|
||||||
:disabled="!startEnabled"
|
|
||||||
icon="pi pi-play"
|
|
||||||
label="START"
|
|
||||||
aria-label="start"
|
|
||||||
size="small"
|
|
||||||
class="m-2.5"
|
|
||||||
@click="startline()"
|
|
||||||
/>
|
|
||||||
</TabList>
|
</TabList>
|
||||||
</div>
|
</div>
|
||||||
<TabPanels class="w-full grow mt-[3rem]">
|
<TabPanels class="w-full grow mt-[3rem]">
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
import InputNumber from 'primevue/inputnumber';
|
import InputNumber from 'primevue/inputnumber';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import RadioButton from 'primevue/radiobutton';
|
import RadioButton from 'primevue/radiobutton';
|
||||||
import Toggle from 'primevue/toggleswitch';
|
import Toggle from 'primevue/toggleswitch';
|
||||||
|
import * as path from '@tauri-apps/api/path';
|
||||||
|
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
|
|
||||||
const store = usePkgStore();
|
const store = usePkgStore();
|
||||||
@ -23,7 +25,32 @@ const cfgRezW = _cfg('rez-w', 1080);
|
|||||||
const cfgRezH = _cfg('rez-h', 1920);
|
const cfgRezH = _cfg('rez-h', 1920);
|
||||||
const cfgDisplayMode = _cfg('display-mode', 'borderless');
|
const cfgDisplayMode = _cfg('display-mode', 'borderless');
|
||||||
const cfgAime = _cfg('aime', false);
|
const cfgAime = _cfg('aime', false);
|
||||||
const cfgAimeCode = _cfg('aime-code', '');
|
|
||||||
|
const aimeCode = ref('');
|
||||||
|
// temp
|
||||||
|
let aimePath = '';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
aimePath = await path.join(
|
||||||
|
await path.dataDir(),
|
||||||
|
'startliner/profile-ongeki-default/aime.txt'
|
||||||
|
);
|
||||||
|
aimeCode.value = await readTextFile(aimePath);
|
||||||
|
})();
|
||||||
|
|
||||||
|
path.homeDir().then(console.log);
|
||||||
|
|
||||||
|
const aimeCodeModel = computed({
|
||||||
|
get() {
|
||||||
|
return aimeCode.value;
|
||||||
|
},
|
||||||
|
async set(value: string) {
|
||||||
|
aimeCode.value = value;
|
||||||
|
if (value.match(/^[0-9]{20}$/)) {
|
||||||
|
await writeTextFile(aimePath, aimeCode.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -98,13 +125,9 @@ const cfgAimeCode = _cfg('aime-code', '');
|
|||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="store.cfg('aime') !== true"
|
:disabled="store.cfg('aime') !== true"
|
||||||
:invalid="
|
|
||||||
store.cfg('aime') === true &&
|
|
||||||
store.cfg('aime-code')?.toString().length !== 20
|
|
||||||
"
|
|
||||||
:maxlength="20"
|
:maxlength="20"
|
||||||
placeholder="00000000000000000000"
|
placeholder="00000000000000000000"
|
||||||
v-model="cfgAimeCode"
|
v-model="aimeCodeModel"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
59
src/components/StartButton.vue
Normal file
59
src/components/StartButton.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Ref, ref } from 'vue';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
|
type StartStatus = 'ready' | 'preparing' | 'running';
|
||||||
|
const startStatus: Ref<StartStatus> = ref('ready');
|
||||||
|
|
||||||
|
const startline = async () => {
|
||||||
|
startStatus.value = 'preparing';
|
||||||
|
await invoke('startline');
|
||||||
|
};
|
||||||
|
|
||||||
|
const kill = async () => {
|
||||||
|
await invoke('kill');
|
||||||
|
startStatus.value = 'ready';
|
||||||
|
};
|
||||||
|
|
||||||
|
listen('launch-start', () => {
|
||||||
|
startStatus.value = 'running';
|
||||||
|
});
|
||||||
|
|
||||||
|
listen('launch-end', () => {
|
||||||
|
startStatus.value = 'ready';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
v-if="startStatus === 'ready'"
|
||||||
|
:disabled="false"
|
||||||
|
icon="pi pi-play"
|
||||||
|
label="START"
|
||||||
|
aria-label="start"
|
||||||
|
size="small"
|
||||||
|
class="m-2.5"
|
||||||
|
@click="startline()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-else-if="startStatus === 'preparing'"
|
||||||
|
disabled
|
||||||
|
icon="pi pi-spin pi-spinner"
|
||||||
|
label="START"
|
||||||
|
aria-label="start"
|
||||||
|
size="small"
|
||||||
|
class="m-2.5"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
:disabled="false"
|
||||||
|
icon="pi pi-ban"
|
||||||
|
label="STOP"
|
||||||
|
aria-label="stop"
|
||||||
|
size="small"
|
||||||
|
class="m-2.5"
|
||||||
|
@click="kill()"
|
||||||
|
/>
|
||||||
|
</template>
|
Reference in New Issue
Block a user