feat: new config format
This commit is contained in:
162
rust/src/profiles/mod.rs
Normal file
162
rust/src/profiles/mod.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use ongeki::OngekiProfile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util};
|
||||
|
||||
pub mod ongeki;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub enum AnyProfile {
|
||||
OngekiProfile(OngekiProfile)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct ProfileMeta {
|
||||
pub game: Game,
|
||||
pub name: String
|
||||
}
|
||||
|
||||
pub trait Profile: Sized {
|
||||
fn new(name: String) -> Result<Self>;
|
||||
fn load(name: String) -> Result<Self>;
|
||||
fn save(&self) -> Result<()>;
|
||||
async fn start(&self, app: AppHandle) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait ProfilePaths {
|
||||
fn config_dir(&self) -> PathBuf;
|
||||
fn data_dir(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
impl AnyProfile {
|
||||
pub fn load(game: Game, name: String) -> Result<Self> {
|
||||
Ok(match game {
|
||||
Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?),
|
||||
Game::Chunithm => panic!("Not implemented")
|
||||
})
|
||||
}
|
||||
pub fn save(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => p.save()
|
||||
}
|
||||
}
|
||||
pub fn meta(&self) -> ProfileMeta {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
ProfileMeta {
|
||||
game: Game::Ongeki,
|
||||
name: p.name.as_ref().unwrap().clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn rename(&mut self, name: String) {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
p.name = Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pkgs(&self) -> &BTreeSet<PkgKey> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => &p.mods
|
||||
}
|
||||
}
|
||||
pub fn pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => &mut p.mods
|
||||
}
|
||||
}
|
||||
pub async fn line_up(&self, app: AppHandle, pkg_hash: String) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
if !p.data_dir().exists() {
|
||||
tokio::fs::create_dir(p.data_dir()).await?;
|
||||
}
|
||||
|
||||
let hash_path = p.data_dir().join(".sl-state");
|
||||
let meta = self.meta();
|
||||
|
||||
p.display.activate(app.clone());
|
||||
|
||||
util::clean_up_opts(p.data_dir().join("option"))?;
|
||||
|
||||
if Self::hash_check(&hash_path, &pkg_hash).await? == true {
|
||||
prepare_packages(&meta, &p.mods).await
|
||||
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||
}
|
||||
let mut ini = p.sgt.line_up(&meta).await
|
||||
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||
p.network.line_up(&mut ini)?;
|
||||
|
||||
ini.write_to_file(p.data_dir().join("segatools.ini"))
|
||||
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||
|
||||
p.bepinex.line_up(&meta)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self, app: AppHandle) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => p.start(app).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn hash_check(prev_hash_path: &impl AsRef<Path>, new_hash: &str) -> Result<bool> {
|
||||
let prev_hash = tokio::fs::read_to_string(&prev_hash_path).await.unwrap_or_default();
|
||||
if prev_hash != new_hash {
|
||||
log::debug!("state {} -> {}", prev_hash, new_hash);
|
||||
tokio::fs::write(prev_hash_path, new_hash).await
|
||||
.map_err(|e| anyhow!("Unable to write the state file: {}", e))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_profiles() -> Result<Vec<ProfileMeta>> {
|
||||
let path = std::fs::read_dir(util::config_dir())?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
|
||||
for f in path {
|
||||
let f = f?;
|
||||
|
||||
if let Ok(meta) = f.metadata() {
|
||||
if !meta.is_dir() {
|
||||
continue;
|
||||
}
|
||||
log::debug!("{:?}", f);
|
||||
if let Some(meta) = meta_from_path(f.path()) {
|
||||
res.push(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn meta_from_path(path: impl AsRef<Path>) -> Option<ProfileMeta> {
|
||||
let regex = regex::Regex::new(
|
||||
r"^profile-([^\-]+)-(.+)$"
|
||||
).expect("Invalid regex");
|
||||
|
||||
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
||||
|
||||
if let Some(caps) = regex.captures(&fname) {
|
||||
let game = caps.get(1).unwrap().as_str();
|
||||
let name = caps.get(2).unwrap().as_str().to_owned();
|
||||
if let Some(game) = Game::from_str(game) {
|
||||
return Some(ProfileMeta { game, name });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
234
rust/src/profiles/ongeki.rs
Normal file
234
rust/src/profiles/ongeki.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use tauri::Emitter;
|
||||
use std::{collections::BTreeSet, path::PathBuf, process::Stdio};
|
||||
use crate::model::config::BepInEx;
|
||||
use crate::util::PathStr;
|
||||
use crate::{model::{config::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util};
|
||||
use super::{Profile, ProfileMeta, ProfilePaths};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct OngekiProfile {
|
||||
// this is an Option only to deceive serde
|
||||
// file serialization doesn't need the name, but IPC does
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
pub mods: BTreeSet<PkgKey>,
|
||||
pub sgt: Segatools,
|
||||
pub display: Display,
|
||||
pub network: Network,
|
||||
pub bepinex: BepInEx
|
||||
}
|
||||
|
||||
impl Profile for OngekiProfile {
|
||||
fn new(name: String) -> Result<Self> {
|
||||
let mut fixed_name = name.trim().replace(" ", "-");
|
||||
|
||||
while util::profile_config_dir(&Game::Ongeki, &name).exists() {
|
||||
fixed_name = format!("new-{}", name);
|
||||
}
|
||||
|
||||
let p = OngekiProfile {
|
||||
name: Some(fixed_name),
|
||||
mods: BTreeSet::new(),
|
||||
sgt: Segatools::default(),
|
||||
display: Display::default(),
|
||||
network: Network::default(),
|
||||
bepinex: BepInEx::default()
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base())?;
|
||||
log::debug!("created profile-ongeki-{}", name);
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn load(name: String) -> Result<Self> {
|
||||
let path = util::profile_config_dir(&Game::Ongeki, &name).join("profile.json");
|
||||
if let Ok(s) = std::fs::read_to_string(&path) {
|
||||
let mut data = serde_json::from_str::<OngekiProfile>(&s)
|
||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||
data.name = Some(name);
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(anyhow!("Unable to open {:?}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self) -> Result<()> {
|
||||
let path = self.config_dir().join("profile.json");
|
||||
|
||||
let mut cpy = self.clone();
|
||||
cpy.name = None;
|
||||
let s = serde_json::to_string_pretty(&cpy)?;
|
||||
if !self.config_dir().exists() {
|
||||
std::fs::create_dir(self.config_dir())
|
||||
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
|
||||
}
|
||||
std::fs::write(&path, s)
|
||||
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
|
||||
log::info!("Written to {}", path.to_string_lossy());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start(&self, app: AppHandle) -> Result<()> {
|
||||
let ini_path = self.data_dir().join("segatools.ini");
|
||||
|
||||
log::debug!("With path {}", ini_path.to_string_lossy());
|
||||
|
||||
let mut game_builder;
|
||||
let mut amd_builder;
|
||||
|
||||
let target_path = PathBuf::from(&self.sgt.target);
|
||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
game_builder = Command::new(exe_dir.join("inject.exe"));
|
||||
amd_builder = Command::new("cmd.exe");
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let wine = p.data.wine_runtime.clone()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/wine"));
|
||||
|
||||
game_builder = Command::new(&wine);
|
||||
amd_builder = Command::new(&wine);
|
||||
|
||||
game_builder.arg(exe_dir.join("inject.exe"));
|
||||
amd_builder.arg("cmd.exe");
|
||||
}
|
||||
|
||||
amd_builder.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args([
|
||||
"/C",
|
||||
&exe_dir.join("inject.exe").stringify()?, "-d", "-k", "mu3hook.dll",
|
||||
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||
]);
|
||||
game_builder
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
ini_path,
|
||||
)
|
||||
.env(
|
||||
"INOHARA_CONFIG_PATH",
|
||||
self.config_dir().join("inohara.cfg"),
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args([
|
||||
"-d", "-k", "mu3hook.dll",
|
||||
"mu3.exe", "-monitor 1",
|
||||
"-screen-width", &self.display.rez.0.to_string(),
|
||||
"-screen-height", &self.display.rez.1.to_string(),
|
||||
"-screen-fullscreen", if self.display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
|
||||
]);
|
||||
|
||||
if self.display.mode == DisplayMode::Borderless {
|
||||
game_builder.arg("-popupwindow");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(||
|
||||
directories::UserDirs::new()
|
||||
.expect("No home directory")
|
||||
.home_dir()
|
||||
.join(".wine")
|
||||
);
|
||||
amd_builder.env("WINEPREFIX", &wineprefix);
|
||||
game_builder.env("WINEPREFIX", &wineprefix);
|
||||
}
|
||||
|
||||
|
||||
let amd_log = File::create(self.data_dir().join("amdaemon.log"))?;
|
||||
let game_log = File::create(self.data_dir().join("mu3.log"))?;
|
||||
|
||||
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(util::CREATE_NO_WINDOW);
|
||||
game_builder.creation_flags(util::CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
if self.sgt.intel == true {
|
||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
||||
}
|
||||
|
||||
util::pkill("amdaemon.exe").await;
|
||||
|
||||
log::info!("Launching amdaemon: {:?}", amd_builder);
|
||||
log::info!("Launching mu3: {:?}", game_builder);
|
||||
|
||||
let mut amd = amd_builder.spawn()?;
|
||||
let mut game = game_builder.spawn()?;
|
||||
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
set.spawn(async move {
|
||||
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon.exe")
|
||||
});
|
||||
|
||||
set.spawn(async move {
|
||||
(game.wait().await.expect("mu3 failed to run"), "mu3.exe")
|
||||
});
|
||||
|
||||
if let Err(e) = app.emit("launch-start", "") {
|
||||
log::warn!("Unable to emit launch-start: {}", e);
|
||||
}
|
||||
|
||||
let (rc, process_name) = set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::info!("{} died with return code {}", process_name, rc);
|
||||
|
||||
if process_name == "amdaemon.exe" {
|
||||
util::pkill("mu3.exe").await;
|
||||
} else {
|
||||
util::pkill("amdaemon.exe").await;
|
||||
}
|
||||
|
||||
set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::debug!("Fin");
|
||||
|
||||
if let Err(e) = app.emit("launch-end", "") {
|
||||
log::warn!("Unable to emit launch-end: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfilePaths for OngekiProfile {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(&Game::Ongeki, &self.name.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &Game::Ongeki, self.name.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfilePaths for ProfileMeta {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(&self.game, &self.name)
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user