feat: port to Microsoft Windows

This commit is contained in:
2025-02-23 18:12:20 +00:00
parent a29bce2227
commit caead1e70f
12 changed files with 145 additions and 26 deletions

BIN
bun.lockb

Binary file not shown.

11
rust/Cargo.lock generated
View File

@ -2359,6 +2359,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "junction"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16"
dependencies = [
"scopeguard",
"windows-sys 0.52.0",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@ -4317,6 +4327,7 @@ dependencies = [
"directories",
"flate2",
"futures",
"junction",
"log",
"regex",
"reqwest",

View File

@ -38,6 +38,7 @@ tauri-plugin-deep-link = "2"
async-std = "1.13.0"
closure = "0.3.0"
derive_more = { version = "2.0.1", features = ["display"] }
junction = "1.2.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }

View File

@ -20,7 +20,7 @@ pub async fn startline(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
if let Some(p) = &appd.profile {
// TODO if p.needsUpdate
liner::line_up(p).await.expect("Line-up failed");
start::start(p).map_err(|e| e.to_string()).map(|_| ())
start::start(p).map_err(|e| { log::error!("Error launching: {}", e.to_string()); e.to_string() }).map(|_| ())
//Ok(())
} else {
Err("No profile".to_owned())
@ -126,12 +126,12 @@ pub async fn save_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
#[tauri::command]
pub async fn init_profile(
state: State<'_, Mutex<AppData>>,
path: PathBuf,
exe_path: PathBuf
) -> Result<Profile, String> {
log::debug!("invoke: init_profile");
log::debug!("invoke: init_profile({})", exe_path.to_string_lossy());
let mut appd = state.lock().await;
let new_profile = Profile::new(path);
let new_profile = Profile::new(exe_path);
new_profile.save().await;
appd.profile = Some(new_profile.clone());

View File

@ -50,6 +50,8 @@ impl DownloadHandler {
cache_file_w.sync_all().await?;
tokio::fs::rename(&zip_path_part, &zip_path).await?;
log::debug!("Downloaded to {}", zip_path.to_string_lossy());
app.emit("download-end", pkg_key)?;
Ok(())

View File

@ -1,9 +1,22 @@
use tokio::task::JoinSet;
use anyhow::Result;
use anyhow::{Result, anyhow};
use tokio::fs;
use std::path::{Path, PathBuf};
use ini::Ini;
use crate::util;
use crate::profile::Profile;
#[cfg(target_os = "linux")]
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
fs::symlink(src, dst).await
}
#[cfg(target_os = "windows")]
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
//std::os::windows::fs::junction_point(src, dst)
junction::create(src, dst)
}
pub async fn line_up(p: &Profile) -> Result<()> {
let dir_out = util::profile_dir(&p);
log::info!("Preparing {}", dir_out.to_string_lossy());
@ -19,9 +32,8 @@ pub async fn line_up(p: &Profile) -> Result<()> {
fs::create_dir_all(dir_out.join("option")).await?;
log::debug!("--");
for m in &p.mods {
log::debug!("{}", m.0);
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 bpx = util::pkg_dir_of(namespace, &name[1..])
@ -35,15 +47,42 @@ pub async fn line_up(p: &Profile) -> Result<()> {
if opt.exists() {
let x = opt.read_dir().unwrap().next().unwrap()?;
if x.metadata()?.is_dir() {
fs::symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
}
}
}
log::debug!("--");
for opt in p.path.join("option").read_dir()? {
// Todo temporary
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 mut opt_dir_in = PathBuf::from(
ini_in.section(Some("vfs"))
.ok_or_else(|| anyhow!("No VFS section in segatools.ini"))?
.get("option")
.ok_or_else(|| anyhow!("No option specified in segatools.ini"))?
);
if opt_dir_in.is_relative() {
opt_dir_in = p.exe_dir.join(opt_dir_in);
}
let opt_dir_out = &dir_out.join("option");
let mut ini_out = ini_in.clone();
ini_out.with_section(Some("vfs")).set(
"option",
util::path_to_str(opt_dir_out)?
);
ini_out.with_section(Some("unity"))
.set("enable", "1")
.set(
"targetAssembly",
util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))?
);
ini_out.write_to_file(dir_out.join("segatools.ini"))?;
log::debug!("Option dir: {} -> {}", opt_dir_in.to_string_lossy(), opt_dir_out.to_string_lossy());
for opt in opt_dir_in.read_dir()? {
let opt = opt?;
fs::symlink(&opt.path(), &dir_out.join("option").join(opt.file_name())).await?;
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
}
Ok(())

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, path::{Path, PathBuf}};
use std::{collections::HashSet, path::PathBuf};
use crate::{model::misc, pkg::PkgKey, util};
use serde::{Deserialize, Serialize};
@ -10,7 +10,7 @@ use tokio::fs;
#[allow(dead_code)]
pub struct Profile {
pub game: misc::Game,
pub path: PathBuf,
pub exe_dir: PathBuf,
pub name: String,
pub mods: HashSet<PkgKey>,
pub wine_runtime: Option<PathBuf>,
@ -18,10 +18,10 @@ pub struct Profile {
}
impl Profile {
pub fn new(path: PathBuf) -> Profile {
pub fn new(exe_path: PathBuf) -> Profile {
Profile {
game: misc::Game::Ongeki,
path: path.parent().unwrap().to_owned(),
exe_dir: exe_path.parent().unwrap().to_owned(),
name: "ongeki-default".to_owned(),
mods: HashSet::new(),

View File

@ -1,5 +1,7 @@
use anyhow::Result;
use tokio::process::{Child, Command};
use std::process::Stdio;
use tokio::task::JoinSet;
use tokio::process::Command;
use crate::profile::Profile;
use crate::util;
@ -11,6 +13,64 @@ pub fn start(p: &Profile) -> Result<Child> {
util::profile_dir(&p).join("segatools.ini"),
)
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
.arg(p.path.join("start.bat"))
.arg(p.exe_dir.join("start.bat"))
.spawn()?)
}
#[cfg(target_os = "windows")]
pub fn start(p: &Profile) -> Result<()> {
let ini_path = util::profile_dir(&p).join("segatools.ini");
log::debug!("With path {}", ini_path.to_string_lossy());
log::info!("Launching amdaemon");
let mut amd = Command::new("cmd.exe")
.env(
"SEGATOOLS_CONFIG_PATH",
&ini_path,
)
.env("OPENSSL_ia32cap", ":~0x20000000")
.current_dir(&p.exe_dir)
.args(["/C", &util::path_to_str(p.exe_dir.join( "inject.exe"))?, "-d", "-k", "mu3hook.dll", "amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"])
// Obviously this is a meme
// Output will be handled properly at a later time
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
log::info!("Launching mu3");
let mut game = Command::new(p.exe_dir.join( "inject.exe"))
.env(
"SEGATOOLS_CONFIG_PATH",
ini_path,
)
.current_dir(&p.exe_dir)
.args(["-d", "-k", "mu3hook.dll", "mu3.exe", "-monitor 1", "-screen-fullscreen", "0", "-popupwindow", "-screen-width", "1080", "-screen-height", "1920"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
tauri::async_runtime::spawn(async move {
let mut set = JoinSet::new();
set.spawn(async move {
amd.wait().await.expect("amdaemon failed to run")
});
set.spawn(async move {
game.wait().await.expect("mu3 failed to run")
});
let res = set.join_next().await.expect("No spawn").expect("No result");
log::info!("One of the processes died with return code {}", res);
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("amdaemon.exe").output().await;
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").output().await;
set.join_next().await.expect("No spawn").expect("No result");
log::debug!("Fin");
});
Ok(())
//Ok((amd, game))
}

View File

@ -1,3 +1,4 @@
use anyhow::{anyhow, Result};
use directories::ProjectDirs;
use std::path::{Path, PathBuf};
@ -31,6 +32,11 @@ pub fn cache_dir() -> PathBuf {
get_dirs().cache_dir().to_owned()
}
pub fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
Ok(p.as_ref().to_str()
.ok_or_else(|| anyhow!("Invalid path: {}", p.as_ref().to_string_lossy()))?.to_owned())
}
pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(&dst).unwrap();
for entry in std::fs::read_dir(src)? {

View File

@ -31,11 +31,11 @@
],
"security": {
"csp": {
"img-src": "'self' asset: https: blob: data:"
"img-src": "'self' asset: https: http://asset.localhost blob: data:"
},
"assetProtocol": {
"enable": true,
"scope": ["**/*", "**/.*/**/*"]
"scope": ["**", "**/*", "**/.*/**/*", "**\\*", "**\\.*\\**\\*"]
}
}
},

View File

@ -24,7 +24,7 @@ const loadProfile = async () => {
await store.reloadProfile();
if (store.profile === null) {
const file = await open({
const exePath = await open({
multiple: false,
directory: false,
filters: [
@ -34,8 +34,8 @@ const loadProfile = async () => {
},
],
});
if (file !== null) {
await store.initProfile(file);
if (exePath !== null) {
await store.initProfile(exePath);
}
}
if (store.profile !== null) {
@ -48,8 +48,8 @@ const loadProfile = async () => {
const isProfileDisabled = computed(() => store.profile === null);
const startline = () => {
invoke('startline');
const startline = async () => {
await invoke('startline');
//startDisabled.value = true;
};

View File

@ -77,8 +77,8 @@ export const usePkgStore = defineStore('pkg', {
Object.assign(this.pkg[key], rv);
},
async initProfile(path: string) {
this.prf = await invoke('init_profile', { path });
async initProfile(exePath: string) {
this.prf = await invoke('init_profile', { exePath });
},
async saveProfile() {