feat: new config format

This commit is contained in:
2025-03-13 23:26:00 +00:00
parent 48dc9ec4df
commit fd27000c05
30 changed files with 1447 additions and 833 deletions

View File

@ -0,0 +1,22 @@
use anyhow::Result;
use ini::Ini;
use crate::{model::config::BepInEx, profiles::ProfilePaths};
impl BepInEx {
pub fn line_up(&self, p: &impl ProfilePaths) -> Result<()> {
let dir = p.data_dir().join("BepInEx");
if dir.exists() && dir.is_dir() {
let dir = dir.join("config");
std::fs::create_dir_all(&dir)?;
let mut ini = Ini::new();
ini.with_section(Some("Logging.Console"))
.set("Enabled", if self.console { "true" } else { "false" });
ini.write_to_file(dir.join("BepInEx.cfg"))?;
}
Ok(())
}
}

139
rust/src/modules/display.rs Normal file
View File

@ -0,0 +1,139 @@
use crate::model::config::{Display, DisplayMode};
use anyhow::Result;
use displayz::{query_displays, DisplaySet};
use tauri::{AppHandle, Listener};
#[cfg(target_os = "windows")]
#[derive(Clone)]
pub struct DisplayInfo {
pub primary: String,
pub set: Option<DisplaySet>
}
impl Default for DisplayInfo {
fn default() -> Self {
DisplayInfo {
primary: "default".to_owned(),
set: query_displays().ok()
}
}
}
impl Default for Display {
fn default() -> Self {
Display {
target: "default".to_owned(),
rez: (1080, 1920),
mode: DisplayMode::Borderless,
rotation: 0,
frequency: 60,
borderless_fullscreen: true,
}
}
}
#[cfg(target_os = "windows")]
impl Display {
pub fn activate(&self, app: AppHandle) {
let display = self.clone();
tauri::async_runtime::spawn(async move {
let info = display.line_up()?;
if let Some(info) = info {
app.listen("launch-end", move |_| {
if let Err(e) = Self::clean_up(&info) {
log::error!("Error cleaning up display: {:?}", e);
}
});
}
Ok::<(), anyhow::Error>(())
});
}
fn line_up(&self) -> Result<Option<DisplayInfo>> {
use anyhow::anyhow;
use displayz::{query_displays, Orientation, Resolution, Frequency};
if self.target == "default" {
log::debug!("prepare display: skip");
return Ok(None);
}
let display_set = query_displays()?;
let primary = display_set
.displays()
.find(|display| display.is_primary())
.ok_or_else(|| anyhow!("Primary display not found"))?;
let target = display_set
.displays()
.find(|display| display.name() == self.target)
.ok_or_else(|| anyhow!("Display {} not found", self.target))?;
target.set_primary()?;
let settings = target.settings()
.as_ref()
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
let res = DisplayInfo {
primary: primary.name().to_owned(),
set: Some(display_set.clone())
};
if self.rotation == 90 || self.rotation == 270 {
let rez = settings.borrow_mut().resolution;
settings.borrow_mut().orientation = if self.rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait };
if rez.height < rez.width {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
}
}
let frequency: u32 = self.frequency
.try_into()
.map_err(|e| anyhow!("Invalid display frequency: {}", e))?;
let width: u32 = self.rez.0
.try_into()
.map_err(|e| anyhow!("Invalid display width: {}", e))?;
let height: u32 = self.rez.1
.try_into()
.map_err(|e| anyhow!("Invalid display height: {}", e))?;
settings.borrow_mut().frequency = Frequency::new(frequency);
if self.borderless_fullscreen && self.mode == DisplayMode::Borderless {
settings.borrow_mut().resolution = Resolution::new(width, height);
}
display_set.apply()?;
displayz::refresh()?;
log::debug!("prepare display: done");
Ok(Some(res))
}
fn clean_up(info: &DisplayInfo) -> Result<()> {
use anyhow::anyhow;
let display_set = info.set.as_ref()
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
let primary = display_set
.displays()
.find(|display| display.name() == info.primary)
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
primary.set_primary()?;
display_set.apply()?;
displayz::refresh()?;
log::debug!("undo display: done");
Ok(())
}
}

5
rust/src/modules/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod display;
pub mod package;
pub mod segatools;
pub mod network;
pub mod bepinex;

View File

@ -0,0 +1,67 @@
use std::{path::PathBuf, process::Command};
use yaml_rust2::YamlLoader;
use anyhow::{Result, anyhow};
use ini::Ini;
use crate::model::config::{Network, NetworkType};
impl Network {
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
log::debug!("begin line-up: network");
ini.with_section(Some("dns")).set("default", &self.remote_address);
let mut section_netenv = ini.with_section(Some("netenv"));
section_netenv.set("enable", "1");
if let Some(suffix) = self.suffix {
section_netenv.set("addrSuffix", suffix.to_string());
}
let mut section_keychip = ini.with_section(Some("keychip"));
if self.subnet.len() > 0 {
section_keychip.set("subnet", &self.subnet);
}
if self.network_type == NetworkType::Artemis {
let network_path = PathBuf::from(&self.local_path);
let artemis_dir = network_path.parent()
.ok_or_else(|| anyhow!("Invalid ARTEMiS path {}", &self.local_path))?;
let cfg_path = artemis_dir.join("config").join("core.yaml");
let cfg = std::fs::read_to_string(&cfg_path)
.map_err(|e| anyhow!("Unable to open core.yaml: {}", e))?;
let cfg = YamlLoader::load_from_str(&cfg)
.map_err(|e| anyhow!("Unable to read core.yaml: {}", e))?;
let cfg = &cfg[0];
log::debug!("{:?}", cfg);
let hostname = &cfg["server"]["hostname"];
let hostname = hostname.clone().into_string();
if let Some(hostname) = hostname {
ini.with_section(Some("dns")).set("default", hostname);
#[cfg(target_os = "windows")]
let mut cmd = Command::new("cmd.exe");
cmd.arg("/C");
if self.local_console == true {
cmd.arg("start");
}
cmd.args(["python", &self.local_path]);
cmd.current_dir(artemis_dir);
cmd.spawn()
.map_err(|e| anyhow!("Unable to spawn artemis: {}", e))?;
} else {
log::warn!("unable to parse the artemis hostname");
}
} else if self.keychip.len() > 0 {
section_keychip.set("id", &self.keychip);
}
log::debug!("end line-up: network");
Ok(())
}
}

View File

@ -0,0 +1,43 @@
use anyhow::Result;
use std::collections::BTreeSet;
use crate::pkg::PkgKey;
use crate::util;
use crate::profiles::ProfilePaths;
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>) -> Result<()> {
log::debug!("begin prepare packages");
let pfx_dir = p.data_dir();
let opt_dir = pfx_dir.join("option");
if pfx_dir.join("BepInEx").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
}
if !opt_dir.exists() {
tokio::fs::create_dir(opt_dir).await?;
}
for m in pkgs {
log::debug!("preparing {}", m);
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
.join("app")
.join("BepInEx");
if bpx_dir.exists() {
util::copy_recursive(&bpx_dir, &pfx_dir.join("BepInEx"))?;
}
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
if opt_dir.exists() {
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
if x.metadata()?.is_dir() {
util::symlink(&x.path(), &pfx_dir.join("option").join(x.file_name())).await?;
}
}
}
log::debug!("end prepare packages");
Ok(())
}

View File

@ -0,0 +1,93 @@
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use ini::Ini;
use crate::{model::{config::Segatools, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
impl Default for Segatools {
fn default() -> Self {
Segatools {
target: PathBuf::default(),
amfs: PathBuf::default(),
option: PathBuf::default(),
appdata: PathBuf::from("appdata"),
enable_aime: false,
intel: false
}
}
}
impl Segatools {
pub async fn line_up(&self, p: &impl ProfilePaths) -> Result<Ini> {
log::debug!("begin line-up: segatools");
let pfx_dir = p.data_dir();
let exe_dir = self.target.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
log::debug!("segatools: {:?} {:?}", pfx_dir, exe_dir);
let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base()).await
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
}
if !pfx_dir.exists() {
tokio::fs::create_dir(&pfx_dir).await
.map_err(|e| anyhow!("Error creating {:?}: {}", pfx_dir, e))?;
}
let ini_in = tokio::fs::read_to_string(&ini_path).await?;
let ini_in = Ini::load_from_str(&ini_in)?;
let opt_dir_out = &pfx_dir.join("option");
let opt_dir_in = if self.option.as_os_str().len() > 0 && self.option.is_relative() {
exe_dir.join(&self.option)
} else {
self.option.clone()
};
let mut ini_out = ini_in.clone();
ini_out.with_section(Some("vfs"))
.set(
"option",
opt_dir_out.stringify()?
)
.set("amfs", self.amfs.stringify()?)
.set("appdata", self.appdata.stringify()?);
ini_out.with_section(Some("unity"))
.set("enable", "1")
.set(
"targetAssembly",
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
);
if self.enable_aime {
ini_out.with_section(Some("aime"))
.set("enable", "1")
.set("aimePath", p.config_dir().join("aime.txt").stringify()?);
} else {
ini_out.with_section(Some("aime"))
.set("enable", "0");
}
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
if !opt_dir_out.exists() {
tokio::fs::create_dir(opt_dir_out).await?;
}
if opt_dir_in.as_os_str().len() > 0 {
for opt in opt_dir_in.read_dir()? {
let opt = opt?;
util::symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
}
}
log::debug!("end line-up: segatools");
Ok(ini_out)
}
}