feat: basic card setup
This commit is contained in:
31
README.md
31
README.md
@ -16,6 +16,7 @@ wipwipwipwipwipwipwipwipwipwipwipwip
|
|||||||
```sh
|
```sh
|
||||||
bun install
|
bun install
|
||||||
bun run tauri dev
|
bun run tauri dev
|
||||||
|
bun run tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package format
|
### Package format
|
||||||
@ -44,16 +45,28 @@ Arbitrary scripts are not supported by design and that will probably never chang
|
|||||||
- Clean data modding
|
- Clean data modding
|
||||||
- Technically multi-platform
|
- Technically multi-platform
|
||||||
|
|
||||||
|
### Architecture details
|
||||||
|
|
||||||
|
- Downloaded packages are stored in `%APPDATA%\7EVENDAYSHOLIDAYS\STARTLINER\data\pkg` (on Linux it's `~/.local/share/startliner` by default).
|
||||||
|
- Each profile is associated with a prefix directory `%APPDATA\7EVENDAYSHOLIDAYS\STARTLINER\data\profile-x` which includes the following:
|
||||||
|
- `option` with junctions of vanilla opts as well as Rainycolor package opts
|
||||||
|
- `BepInEx` with Rainycolor mods copied over
|
||||||
|
- It's currently pointless to symlink those since they are measured in kilobytes
|
||||||
|
- `segatools.ini` generated on-the-fly and pointing at `option`
|
||||||
|
- It's currently based on the existing `segatools.ini` but that will change in the future
|
||||||
|
- `aime.txt` (if enabled)
|
||||||
|
|
||||||
### Todo
|
### Todo
|
||||||
|
|
||||||
- Updates and auto-updates
|
- Auto-updates
|
||||||
- Run from CLI
|
- Running from CLI
|
||||||
- Support CHUNITHM
|
- Multiple profiles
|
||||||
- Support segatools as a special package
|
- CHUNITHM support
|
||||||
- Progress bars
|
- segatools as a special package
|
||||||
- Only rebuild the profile when needed
|
- Progress bars and other GUI sugar
|
||||||
|
- Rebuilding the profile only when necessary
|
||||||
|
|
||||||
## Endgame
|
### Endgame
|
||||||
|
|
||||||
- Support IO DLLs and artemis as special packages.
|
- IO DLLs and artemis as special packages
|
||||||
- Support other arcade games (if there is demand).
|
- Other arcade games (if there is demand)
|
||||||
|
@ -16,13 +16,13 @@ impl AppData {
|
|||||||
.ok_or_else(|| anyhow!("No profile"))?;
|
.ok_or_else(|| anyhow!("No profile"))?;
|
||||||
|
|
||||||
if enable {
|
if enable {
|
||||||
let pkg = self.pkgs.get(key.clone())?;
|
let pkg = self.pkgs.get(&key)?;
|
||||||
let loc = pkg.loc
|
let loc = pkg.loc
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||||
profile.mods.insert(key);
|
profile.mods.insert(key);
|
||||||
for d in &loc.dependencies {
|
for d in &loc.dependencies {
|
||||||
self.toggle_package(d.clone(), true)?;
|
_ = self.toggle_package(d.clone(), true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile.mods.remove(&key);
|
profile.mods.remove(&key);
|
||||||
|
@ -55,7 +55,7 @@ pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: Pkg
|
|||||||
log::debug!("invoke: get_package({})", key);
|
log::debug!("invoke: get_package({})", key);
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
appd.pkgs.get(key)
|
appd.pkgs.get(&key)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
@ -93,16 +93,6 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.listen("install-end", closure!(clone apph, |ev| {
|
|
||||||
let payload = serde_json::from_str::<pkg_store::Payload>(&ev.payload());
|
|
||||||
let apph = apph.clone();
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
|
||||||
let mut appd = mutex.lock().await;
|
|
||||||
_ = appd.toggle_package(payload.unwrap().pkg, true);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
@ -13,7 +13,7 @@ async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Resul
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
//std::os::windows::fs::junction_point(src, dst)
|
//std::os::windows::fs::junction_point(src, dst) // is unstable
|
||||||
junction::create(src, dst)
|
junction::create(src, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +77,13 @@ pub async fn line_up(p: &Profile) -> Result<()> {
|
|||||||
"targetAssembly",
|
"targetAssembly",
|
||||||
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) {
|
||||||
|
ini_out.with_section(Some("aime"))
|
||||||
|
.set("enable", "1")
|
||||||
|
.set("aimePath", util::path_to_str(dir_out.join("aime.txt"))?);
|
||||||
|
}
|
||||||
|
|
||||||
ini_out.write_to_file(dir_out.join("segatools.ini"))?;
|
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.to_string_lossy(), opt_dir_out.to_string_lossy());
|
||||||
@ -87,3 +94,15 @@ 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)
|
||||||
|
}
|
@ -24,10 +24,11 @@ pub struct Package {
|
|||||||
pub rmt: Option<Remote>
|
pub rmt: Option<Remote>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Kind {
|
pub enum Kind {
|
||||||
|
Unchecked,
|
||||||
#[default] Mod,
|
#[default] Mod,
|
||||||
UnsupportedMod
|
Unsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
|
@ -37,8 +37,8 @@ impl PackageStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: PkgKey) -> Result<&Package> {
|
pub fn get(&self, key: &PkgKey) -> Result<&Package> {
|
||||||
self.store.get(&key)
|
self.store.get(key)
|
||||||
.ok_or_else(|| anyhow!("Invalid package key"))
|
.ok_or_else(|| anyhow!("Invalid package key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ impl PackageStore {
|
|||||||
pub async fn reload_package(&mut self, key: PkgKey) {
|
pub async fn reload_package(&mut self, key: PkgKey) {
|
||||||
let dir = util::pkg_dir().join(&key.0);
|
let dir = util::pkg_dir().join(&key.0);
|
||||||
if let Ok(pkg) = Package::from_dir(dir).await {
|
if let Ok(pkg) = Package::from_dir(dir).await {
|
||||||
self.update_package(key, pkg);
|
self.update_nonremote(key, pkg);
|
||||||
} else {
|
} else {
|
||||||
log::error!("couldn't reload {}", key);
|
log::error!("couldn't reload {}", key);
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ impl PackageStore {
|
|||||||
|
|
||||||
while let Some(res) = futures.join_next().await {
|
while let Some(res) = futures.join_next().await {
|
||||||
if let Ok(Ok(pkg)) = res {
|
if let Ok(Ok(pkg)) = res {
|
||||||
self.update_package(pkg.key(), pkg);
|
self.update_nonremote(pkg.key(), pkg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,10 +203,11 @@ impl PackageStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_package(&mut self, key: PkgKey, mut new: Package) {
|
fn update_nonremote(&mut self, key: PkgKey, mut new: Package) {
|
||||||
if let Some(old) = self.store.get(&key) {
|
if let Some(old) = self.store.remove(&key) {
|
||||||
new.rmt = old.rmt.clone();
|
new.rmt = old.rmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store.insert(key, new);
|
self.store.insert(key, new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ pub struct Profile {
|
|||||||
pub mods: HashSet<PkgKey>,
|
pub mods: HashSet<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
|
||||||
|
// eventually it should become strict
|
||||||
pub cfg: HashMap<String, serde_json::Value>
|
pub cfg: HashMap<String, serde_json::Value>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,4 +64,23 @@ impl Profile {
|
|||||||
fs::write(&path, s).await.unwrap();
|
fs::write(&path, s).await.unwrap();
|
||||||
log::info!("Written to {}", path.to_string_lossy());
|
log::info!("Written to {}", path.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
||||||
|
self.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)
|
||||||
|
.and_then(|c| c.as_i64())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_str(&self, key: &str, default: &str) -> String {
|
||||||
|
self.cfg.get(key)
|
||||||
|
.and_then(|c| c.as_str())
|
||||||
|
.unwrap_or(default)
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,41 +44,62 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
log::info!("Launching amdaemon");
|
log::info!("Launching amdaemon");
|
||||||
|
|
||||||
let mut amd_builder = Command::new("cmd.exe");
|
let mut amd_builder = Command::new("cmd.exe");
|
||||||
|
let mut game_builder = Command::new(p.exe_dir.join("inject.exe"));
|
||||||
|
|
||||||
|
let display_mode = p.get_str("display-mode", "borderless");
|
||||||
|
|
||||||
amd_builder.env(
|
amd_builder.env(
|
||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
&ini_path,
|
&ini_path,
|
||||||
)
|
)
|
||||||
.creation_flags(create_no_window)
|
|
||||||
.current_dir(&p.exe_dir)
|
.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"])
|
.args([
|
||||||
// Obviously this is a meme
|
"/C",
|
||||||
// Output will be handled properly at a later time
|
&util::path_to_str(p.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
||||||
.stdout(Stdio::null())
|
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||||
.stderr(Stdio::null());
|
]);
|
||||||
|
game_builder
|
||||||
if let Some(v) = p.cfg.get("intel") {
|
|
||||||
if v == true {
|
|
||||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut amd = amd_builder.spawn()?;
|
|
||||||
|
|
||||||
log::info!("Launching mu3");
|
|
||||||
let mut game = Command::new(p.exe_dir.join( "inject.exe"))
|
|
||||||
.env(
|
.env(
|
||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
ini_path,
|
ini_path,
|
||||||
)
|
)
|
||||||
.creation_flags(create_no_window)
|
|
||||||
.current_dir(&p.exe_dir)
|
.current_dir(&p.exe_dir)
|
||||||
.args(["-d", "-k", "mu3hook.dll", "mu3.exe", "-monitor 1", "-screen-fullscreen", "0", "-popupwindow", "-screen-width", "1080", "-screen-height", "1920"])
|
.args([
|
||||||
.stdout(Stdio::null())
|
"-d", "-k", "mu3hook.dll",
|
||||||
.stderr(Stdio::null())
|
"mu3.exe", "-monitor 1",
|
||||||
.spawn()?;
|
"-screen-width", &p.get_int("rez-w", 1080).to_string(),
|
||||||
|
"-screen-height", &p.get_int("rez-h", 1920).to_string(),
|
||||||
|
"-screen-fullscreen", if display_mode == "fullscreen" { "1" } else { "0" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
if display_mode == "borderless" {
|
||||||
|
game_builder.arg("-popupwindow");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
amd_builder
|
||||||
|
.creation_flags(create_no_window)
|
||||||
|
// Obviously, this is a meme
|
||||||
|
// Output will be handled properly at a later time
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
|
game_builder
|
||||||
|
.creation_flags(create_no_window)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.get_bool("intel", false) == true {
|
||||||
|
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut amd = amd_builder.spawn()?;
|
||||||
|
let mut game = game_builder.spawn()?;
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
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")
|
||||||
});
|
});
|
||||||
@ -91,8 +112,8 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
|
|
||||||
log::info!("One of the processes died with return code {}", res);
|
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("amdaemon.exe").creation_flags(create_no_window).output().await;
|
||||||
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").output().await;
|
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").creation_flags(create_no_window).output().await;
|
||||||
|
|
||||||
set.join_next().await.expect("No spawn").expect("No result");
|
set.join_next().await.expect("No spawn").expect("No result");
|
||||||
|
|
||||||
@ -102,5 +123,4 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
//Ok((amd, game))
|
|
||||||
}
|
}
|
@ -13,7 +13,12 @@ const pkgs = usePkgStore();
|
|||||||
const group = () => {
|
const group = () => {
|
||||||
const a = Object.assign(
|
const a = Object.assign(
|
||||||
{},
|
{},
|
||||||
Object.groupBy(pkgs.allLocal, ({ namespace }) => namespace)
|
Object.groupBy(
|
||||||
|
pkgs.allLocal
|
||||||
|
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||||
|
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||||
|
({ namespace }) => namespace
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
@ -22,18 +22,18 @@ const cfgIntel = _cfg('intel', false);
|
|||||||
const cfgRezW = _cfg('rez-w', 1080);
|
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', true);
|
const cfgAime = _cfg('aime', false);
|
||||||
const cfgAimeCode = _cfg('aime-code', '00001111222233334444');
|
const cfgAimeCode = _cfg('aime-code', '');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Fieldset legend="Launch options" :toggleable="true">
|
<Fieldset legend="Launch options" :toggleable="true">
|
||||||
<div class="flex w-full flex-col gap-1">
|
<div class="flex w-full flex-col gap-1">
|
||||||
<label for="switch" class="flex flex-row w-full p-2">
|
<label for="switch1" class="flex flex-row w-full p-2">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
OpenSSL crash workaround for Intel ≥10th gen
|
OpenSSL bug workaround for Intel ≥10th gen
|
||||||
</div>
|
</div>
|
||||||
<Toggle inputId="switch" v-model="cfgIntel" />
|
<Toggle inputId="switch1" v-model="cfgIntel" />
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
id="resolution"
|
id="resolution"
|
||||||
@ -88,17 +88,20 @@ const cfgAimeCode = _cfg('aime-code', '00001111222233334444');
|
|||||||
<label for="ingredient3">fullscreen</label>
|
<label for="ingredient3">fullscreen</label>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label for="switch" class="flex flex-row w-full p-2">
|
<label for="switch2" class="flex flex-row w-full p-2">
|
||||||
<div class="grow">Aime emulation</div>
|
<div class="grow">Aime emulation</div>
|
||||||
<Toggle inputId="switch" v-model="cfgAime" />
|
<Toggle inputId="switch2" v-model="cfgAime" />
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-row w-full p-2 items-center">
|
<label class="flex flex-row w-full p-2 items-center">
|
||||||
<div class="grow">Aime code</div>
|
<div class="grow">Aime code</div>
|
||||||
<InputText
|
<InputText
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="store.cfg('aime') === false"
|
:disabled="store.cfg('aime') !== true"
|
||||||
:minlength="20"
|
: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="cfgAimeCode"
|
||||||
|
@ -33,7 +33,6 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
const key = ev.payload.pkg;
|
const key = ev.payload.pkg;
|
||||||
await this.reload(key);
|
await this.reload(key);
|
||||||
this.pkg[key].js.busy = true;
|
this.pkg[key].js.busy = true;
|
||||||
console.log('install-start' + key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
listen<InstallStatus>('install-end', async (ev) => {
|
listen<InstallStatus>('install-end', async (ev) => {
|
||||||
@ -41,7 +40,6 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
await this.reload(key);
|
await this.reload(key);
|
||||||
await this.reloadProfile();
|
await this.reloadProfile();
|
||||||
this.pkg[key].js.busy = false;
|
this.pkg[key].js.busy = false;
|
||||||
console.log('install-end' + key);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user