forked from akanyan/STARTLINER
feat: groundwork for multi-profile support
This commit is contained in:
@ -1,16 +1,64 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use crate::pkg::PkgKey;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
use crate::pkg_store::PackageStore;
|
||||
use crate::Profile;
|
||||
use crate::{util, Profile};
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct GlobalConfig {
|
||||
pub recent_profile: Option<(Game, String)>
|
||||
}
|
||||
|
||||
pub struct AppData {
|
||||
pub profile: Option<Profile>,
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
pub fn new(app: AppHandle) -> AppData {
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("config.json");
|
||||
|
||||
let cfg = std::fs::read_to_string(&path)
|
||||
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile = match cfg.recent_profile {
|
||||
Some((ref game, ref name)) => Profile::load(game, name).ok(),
|
||||
None => None
|
||||
};
|
||||
|
||||
AppData {
|
||||
profile,
|
||||
pkgs: PackageStore::new(app),
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self) -> Result<(), std::io::Error> {
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("config.json");
|
||||
|
||||
std::fs::write(&path, serde_json::to_string(&self.cfg)?)
|
||||
}
|
||||
|
||||
pub fn switch_profile(&mut self, game: &Game, name: &str) -> Result<()> {
|
||||
self.profile = Profile::load(game, name).ok();
|
||||
if self.profile.is_some() {
|
||||
self.cfg.recent_profile = Some((game.to_owned(), name.to_owned()));
|
||||
} else {
|
||||
self.cfg.recent_profile = None;
|
||||
}
|
||||
self.write()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_package(&mut self, key: PkgKey, enable: bool) -> Result<()> {
|
||||
log::debug!("toggle: {} {}", key, enable);
|
||||
|
||||
@ -22,12 +70,12 @@ impl AppData {
|
||||
let loc = pkg.loc
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||
profile.mods.insert(key);
|
||||
profile.data.mods.insert(key);
|
||||
for d in &loc.dependencies {
|
||||
_ = self.toggle_package(d.clone(), true);
|
||||
}
|
||||
} else {
|
||||
profile.mods.remove(&key);
|
||||
profile.data.mods.remove(&key);
|
||||
for (ckey, pkg) in self.pkgs.get_all() {
|
||||
if let Some(loc) = pkg.loc {
|
||||
if loc.dependencies.contains(&key) {
|
||||
@ -42,7 +90,7 @@ impl AppData {
|
||||
|
||||
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for pkg in &p.mods {
|
||||
for pkg in &p.data.mods {
|
||||
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
||||
pkg.hash(&mut hasher);
|
||||
x.version.hash(&mut hasher);
|
||||
|
@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::model::misc::Game;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::pkg_store::InstallResult;
|
||||
use crate::profile::Profile;
|
||||
@ -106,6 +107,23 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_profiles() -> Result<Vec<(Game, String)>, String> {
|
||||
log::debug!("invoke: list_profiles");
|
||||
|
||||
let list = Profile::list().await.map_err(|e| e.to_string())?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: String) -> Result<(), String> {
|
||||
log::debug!("invoke: load_profile({} {:?})", game, name);
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.switch_profile(&game, &name).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
||||
log::debug!("invoke: get_current_profile");
|
||||
@ -115,8 +133,8 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
||||
log::debug!("invoke: save_profile");
|
||||
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
||||
log::debug!("invoke: save_current_profile");
|
||||
|
||||
let appd = state.lock().await;
|
||||
if let Some(p) = &appd.profile {
|
||||
@ -133,18 +151,17 @@ pub async fn init_profile(
|
||||
state: State<'_, Mutex<AppData>>,
|
||||
exe_path: PathBuf
|
||||
) -> Result<Profile, String> {
|
||||
log::debug!("invoke: init_profile({})", exe_path.to_string_lossy());
|
||||
log::debug!("invoke: init_profile({:?})", exe_path);
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
let new_profile = Profile::new(exe_path);
|
||||
if let Some(new_profile) = Profile::new(exe_path) {
|
||||
new_profile.save().await;
|
||||
appd.profile = Some(new_profile.clone());
|
||||
|
||||
new_profile.save().await;
|
||||
appd.profile = Some(new_profile.clone());
|
||||
|
||||
fs::create_dir(new_profile.dir()).await
|
||||
.map_err(|e| format!("Unable to create profile directory: {}", e))?;
|
||||
|
||||
Ok(new_profile)
|
||||
Ok(new_profile)
|
||||
} else {
|
||||
Err("Unrecognized game".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
// #[tauri::command]
|
||||
@ -184,7 +201,7 @@ pub async fn write_profile_data(
|
||||
content: String
|
||||
) -> Result<(), String> {
|
||||
let appd = state.lock().await;
|
||||
|
||||
|
||||
if let Some(p) = &appd.profile {
|
||||
fs::write(p.dir().join(&path), content).await
|
||||
.map_err(|e| format!("Unable to write to {:?}: {}", path, e))?;
|
||||
@ -204,7 +221,7 @@ pub async fn set_cfg(
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
if let Some(p) = &mut appd.profile {
|
||||
p.cfg.insert(key, value);
|
||||
p.data.cfg.insert(key, value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -12,7 +12,6 @@ mod appdata;
|
||||
use closure::closure;
|
||||
use appdata::AppData;
|
||||
use pkg::PkgKey;
|
||||
use pkg_store::PackageStore;
|
||||
use profile::Profile;
|
||||
use tauri::{Listener, Manager};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
@ -47,6 +46,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
// Todo deindent this chimera
|
||||
let url = &args[1];
|
||||
if &url[..13] == "rainycolor://" {
|
||||
log::info!("Deep link: {}", url);
|
||||
let regex = regex::Regex::new(
|
||||
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
||||
).expect("Invalid regex");
|
||||
@ -72,10 +72,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
let app_data = AppData {
|
||||
profile: Profile::load(),
|
||||
pkgs: PackageStore::new(app.handle().clone())
|
||||
};
|
||||
let app_data = AppData::new(app.handle().clone());
|
||||
|
||||
app.manage(Mutex::new(app_data));
|
||||
app.deep_link().register_all()?;
|
||||
@ -103,9 +100,11 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::install_package,
|
||||
cmd::delete_package,
|
||||
cmd::toggle_package,
|
||||
cmd::get_current_profile,
|
||||
cmd::list_profiles,
|
||||
cmd::init_profile,
|
||||
cmd::save_profile,
|
||||
cmd::load_profile,
|
||||
cmd::get_current_profile,
|
||||
cmd::save_current_profile,
|
||||
cmd::read_profile_data,
|
||||
cmd::write_profile_data,
|
||||
cmd::startline,
|
||||
|
@ -46,7 +46,7 @@ async fn prepare_packages(p: &Profile) -> Result<()> {
|
||||
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
||||
}
|
||||
|
||||
for m in &p.mods {
|
||||
for m in &p.data.mods {
|
||||
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
|
||||
@ -65,13 +65,15 @@ async fn prepare_packages(p: &Profile) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("prepare packages: done");
|
||||
|
||||
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.data.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"))
|
||||
@ -80,7 +82,7 @@ pub async fn prepare_config(p: &Profile) -> Result<()> {
|
||||
.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);
|
||||
opt_dir_in = p.data.exe_dir.join(opt_dir_in);
|
||||
}
|
||||
let opt_dir_out = &dir_out.join("option");
|
||||
|
||||
@ -104,11 +106,13 @@ pub async fn prepare_config(p: &Profile) -> Result<()> {
|
||||
|
||||
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());
|
||||
log::debug!("Option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||
for opt in opt_dir_in.read_dir()? {
|
||||
let opt = opt?;
|
||||
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
||||
}
|
||||
|
||||
log::debug!("prepare config: done");
|
||||
|
||||
Ok(())
|
||||
}
|
@ -2,6 +2,27 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum Game {
|
||||
#[serde(rename = "ongeki")]
|
||||
Ongeki,
|
||||
#[serde(rename = "chunithm")]
|
||||
Chunithm,
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn from_str(s: &str) -> Option<Game> {
|
||||
match s {
|
||||
"ongeki" => Some(Game::Ongeki),
|
||||
"chunithm" => Some(Game::Chunithm),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Game {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Game::Ongeki => write!(f, "ongeki"),
|
||||
Game::Chunithm => write!(f, "chunithm")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use std::{collections::{BTreeSet, HashMap}, path::PathBuf};
|
||||
use crate::{model::misc, pkg::PkgKey, util};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::{collections::{BTreeSet, HashMap}, path::{Path, PathBuf}};
|
||||
use crate::{model::misc::{self, Game}, pkg::PkgKey, util};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
// {game}-profile-{name}.json
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Profile {
|
||||
pub game: misc::Game,
|
||||
pub exe_dir: PathBuf,
|
||||
pub name: String,
|
||||
pub data: ProfileData
|
||||
}
|
||||
|
||||
// The contents of profile-{game}-{name}.json
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct ProfileData {
|
||||
pub exe_dir: PathBuf,
|
||||
pub mods: BTreeSet<PkgKey>,
|
||||
pub wine_runtime: Option<PathBuf>,
|
||||
pub wine_prefix: Option<PathBuf>,
|
||||
@ -21,80 +24,126 @@ pub struct Profile {
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(exe_path: PathBuf) -> Profile {
|
||||
Profile {
|
||||
game: misc::Game::Ongeki,
|
||||
exe_dir: exe_path.parent().unwrap().to_owned(),
|
||||
name: "ongeki-default".to_owned(),
|
||||
mods: BTreeSet::new(),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()),
|
||||
#[cfg(target_os = "windows")]
|
||||
wine_runtime: None,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
wine_prefix: Some(
|
||||
directories::UserDirs::new()
|
||||
.expect("No home directory")
|
||||
.home_dir()
|
||||
.join(".wine"),
|
||||
),
|
||||
#[cfg(target_os = "windows")]
|
||||
wine_prefix: None,
|
||||
cfg: HashMap::new()
|
||||
pub fn new(exe_path: PathBuf) -> Option<Profile> {
|
||||
let game;
|
||||
if exe_path.ends_with("mu3.exe") {
|
||||
game = misc::Game::Ongeki
|
||||
} else if exe_path.ends_with("chusanApp.exe") {
|
||||
// game = misc::Game::Chunithm;
|
||||
return None;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Profile {
|
||||
name: format!("{}", "default"),
|
||||
game,
|
||||
data: ProfileData {
|
||||
exe_dir: exe_path.parent().unwrap().to_owned(),
|
||||
mods: BTreeSet::new(),
|
||||
wine_runtime: None,
|
||||
wine_prefix: None,
|
||||
cfg: HashMap::new()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dir(&self) -> PathBuf {
|
||||
util::get_dirs()
|
||||
.data_dir()
|
||||
.join("profile-".to_owned() + &self.name)
|
||||
.join(format!("profile-{}-{}", self.game, self.name))
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn load() -> Option<Profile> {
|
||||
pub async fn list() -> Result<Vec<(Game, String)>> {
|
||||
let path = std::fs::read_dir(
|
||||
util::get_dirs().config_dir()
|
||||
)?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
|
||||
for f in path {
|
||||
let f = f?;
|
||||
|
||||
if let Some(pair) = Self::name_from_path(f.path()) {
|
||||
res.push(pair);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn load(game: &Game, name: &str) -> Result<Profile> {
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("profile-ongeki-default.json");
|
||||
if let Ok(s) = std::fs::read_to_string(path) {
|
||||
Some(serde_json::from_str(&s).expect("Invalid profile json"))
|
||||
.join(format!("profile-{}-{}.json", game, name));
|
||||
if let Ok(s) = std::fs::read_to_string(&path) {
|
||||
let (game, name) = Self::name_from_path(&path)
|
||||
.ok_or_else(|| anyhow!("Invalid filename: {:?}", path.file_name()))?;
|
||||
|
||||
let data = serde_json::from_str::<ProfileData>(&s)
|
||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||
|
||||
Ok(Profile {
|
||||
game,
|
||||
name,
|
||||
data
|
||||
})
|
||||
} else {
|
||||
None
|
||||
Err(anyhow!("Unable to open {:?}", path))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self) {
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("profile-ongeki-default.json");
|
||||
let s = serde_json::to_string_pretty(self).unwrap();
|
||||
.join(format!("profile-{}-{}.json", self.game, self.name));
|
||||
|
||||
let s = serde_json::to_string_pretty(&self.data).unwrap();
|
||||
fs::write(&path, s).await.unwrap();
|
||||
log::info!("Written to {}", path.to_string_lossy());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> {
|
||||
self.cfg.get(key)
|
||||
self.data.cfg.get(key)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key))
|
||||
}
|
||||
|
||||
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
||||
self.cfg.get(key)
|
||||
self.data.cfg.get(key)
|
||||
.and_then(|c| c.as_bool())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn get_int(&self, key: &str, default: i64) -> i64 {
|
||||
self.cfg.get(key)
|
||||
self.data.cfg.get(key)
|
||||
.and_then(|c| c.as_i64())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn get_str(&self, key: &str, default: &str) -> String {
|
||||
self.cfg.get(key)
|
||||
self.data.cfg.get(key)
|
||||
.and_then(|c| c.as_str())
|
||||
.unwrap_or(default)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn name_from_path(path: impl AsRef<Path>) -> Option<(Game, String)> {
|
||||
let regex = regex::Regex::new(
|
||||
r"profile-([^\-]+)-([^\-]+)\.json"
|
||||
).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((game, name));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use std::process::Stdio;
|
||||
@ -26,13 +27,13 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let wine = p.wine_runtime.as_ref()
|
||||
.expect("No wine path specified");
|
||||
let wine = p.data.wine_runtime.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("/usr/bin/wine"));
|
||||
|
||||
game_builder = Command::new(wine);
|
||||
amd_builder = Command::new(wine);
|
||||
game_builder = Command::new(&wine);
|
||||
amd_builder = Command::new(&wine);
|
||||
|
||||
game_builder.arg(p.exe_dir.join("inject.exe"));
|
||||
game_builder.arg(p.data.exe_dir.join("inject.exe"));
|
||||
amd_builder.arg("cmd.exe");
|
||||
}
|
||||
|
||||
@ -42,10 +43,10 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&p.exe_dir)
|
||||
.current_dir(&p.data.exe_dir)
|
||||
.args([
|
||||
"/C",
|
||||
&util::path_to_str(p.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
||||
&util::path_to_str(p.data.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
||||
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||
]);
|
||||
game_builder
|
||||
@ -53,7 +54,7 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
ini_path,
|
||||
)
|
||||
.current_dir(&p.exe_dir)
|
||||
.current_dir(&p.data.exe_dir)
|
||||
.args([
|
||||
"-d", "-k", "mu3hook.dll",
|
||||
"mu3.exe", "-monitor 1",
|
||||
@ -68,10 +69,14 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let wineprefix = p.wine_prefix.as_ref()
|
||||
.expect("No wineprefix specified");
|
||||
amd_builder.env("WINEPREFIX", wineprefix);
|
||||
game_builder.env("WINEPREFIX", wineprefix);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user