forked from akanyan/STARTLINER
feat: basic card setup
This commit is contained in:
31
README.md
31
README.md
@ -16,6 +16,7 @@ wipwipwipwipwipwipwipwipwipwipwipwip
|
||||
```sh
|
||||
bun install
|
||||
bun run tauri dev
|
||||
bun run tauri build
|
||||
```
|
||||
|
||||
### Package format
|
||||
@ -44,16 +45,28 @@ Arbitrary scripts are not supported by design and that will probably never chang
|
||||
- Clean data modding
|
||||
- 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
|
||||
|
||||
- Updates and auto-updates
|
||||
- Run from CLI
|
||||
- Support CHUNITHM
|
||||
- Support segatools as a special package
|
||||
- Progress bars
|
||||
- Only rebuild the profile when needed
|
||||
- Auto-updates
|
||||
- Running from CLI
|
||||
- Multiple profiles
|
||||
- CHUNITHM support
|
||||
- segatools as a special package
|
||||
- Progress bars and other GUI sugar
|
||||
- Rebuilding the profile only when necessary
|
||||
|
||||
## Endgame
|
||||
### Endgame
|
||||
|
||||
- Support IO DLLs and artemis as special packages.
|
||||
- Support other arcade games (if there is demand).
|
||||
- IO DLLs and artemis as special packages
|
||||
- Other arcade games (if there is demand)
|
||||
|
@ -16,13 +16,13 @@ impl AppData {
|
||||
.ok_or_else(|| anyhow!("No profile"))?;
|
||||
|
||||
if enable {
|
||||
let pkg = self.pkgs.get(key.clone())?;
|
||||
let pkg = self.pkgs.get(&key)?;
|
||||
let loc = pkg.loc
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||
profile.mods.insert(key);
|
||||
for d in &loc.dependencies {
|
||||
self.toggle_package(d.clone(), true)?;
|
||||
_ = self.toggle_package(d.clone(), true);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
|
||||
let appd = state.lock().await;
|
||||
appd.pkgs.get(key)
|
||||
appd.pkgs.get(&key)
|
||||
.map_err(|e| e.to_string())
|
||||
.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(())
|
||||
})
|
||||
.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")]
|
||||
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)
|
||||
}
|
||||
|
||||
@ -77,6 +77,13 @@ pub async fn line_up(p: &Profile) -> Result<()> {
|
||||
"targetAssembly",
|
||||
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"))?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Kind {
|
||||
Unchecked,
|
||||
#[default] Mod,
|
||||
UnsupportedMod
|
||||
Unsupported
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
|
@ -37,8 +37,8 @@ impl PackageStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: PkgKey) -> Result<&Package> {
|
||||
self.store.get(&key)
|
||||
pub fn get(&self, key: &PkgKey) -> Result<&Package> {
|
||||
self.store.get(key)
|
||||
.ok_or_else(|| anyhow!("Invalid package key"))
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ impl PackageStore {
|
||||
pub async fn reload_package(&mut self, key: PkgKey) {
|
||||
let dir = util::pkg_dir().join(&key.0);
|
||||
if let Ok(pkg) = Package::from_dir(dir).await {
|
||||
self.update_package(key, pkg);
|
||||
self.update_nonremote(key, pkg);
|
||||
} else {
|
||||
log::error!("couldn't reload {}", key);
|
||||
}
|
||||
@ -68,7 +68,7 @@ impl PackageStore {
|
||||
|
||||
while let Some(res) = futures.join_next().await {
|
||||
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) {
|
||||
if let Some(old) = self.store.get(&key) {
|
||||
new.rmt = old.rmt.clone();
|
||||
fn update_nonremote(&mut self, key: PkgKey, mut new: Package) {
|
||||
if let Some(old) = self.store.remove(&key) {
|
||||
new.rmt = old.rmt;
|
||||
}
|
||||
|
||||
self.store.insert(key, new);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@ pub struct Profile {
|
||||
pub mods: HashSet<PkgKey>,
|
||||
pub wine_runtime: 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>
|
||||
}
|
||||
|
||||
@ -62,4 +64,23 @@ impl Profile {
|
||||
fs::write(&path, s).await.unwrap();
|
||||
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");
|
||||
|
||||
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(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
&ini_path,
|
||||
)
|
||||
.creation_flags(create_no_window)
|
||||
.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());
|
||||
|
||||
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"))
|
||||
.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"
|
||||
]);
|
||||
game_builder
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
ini_path,
|
||||
)
|
||||
.creation_flags(create_no_window)
|
||||
.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()?;
|
||||
.args([
|
||||
"-d", "-k", "mu3hook.dll",
|
||||
"mu3.exe", "-monitor 1",
|
||||
"-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 {
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
set.spawn(async move {
|
||||
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);
|
||||
|
||||
_ = 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;
|
||||
_ = 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").creation_flags(create_no_window).output().await;
|
||||
|
||||
set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
@ -102,5 +123,4 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||
});
|
||||
|
||||
Ok(())
|
||||
//Ok((amd, game))
|
||||
}
|
@ -13,7 +13,12 @@ const pkgs = usePkgStore();
|
||||
const group = () => {
|
||||
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;
|
||||
};
|
||||
|
@ -22,18 +22,18 @@ const cfgIntel = _cfg('intel', false);
|
||||
const cfgRezW = _cfg('rez-w', 1080);
|
||||
const cfgRezH = _cfg('rez-h', 1920);
|
||||
const cfgDisplayMode = _cfg('display-mode', 'borderless');
|
||||
const cfgAime = _cfg('aime', true);
|
||||
const cfgAimeCode = _cfg('aime-code', '00001111222233334444');
|
||||
const cfgAime = _cfg('aime', false);
|
||||
const cfgAimeCode = _cfg('aime-code', '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset legend="Launch options" :toggleable="true">
|
||||
<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">
|
||||
OpenSSL crash workaround for Intel ≥10th gen
|
||||
OpenSSL bug workaround for Intel ≥10th gen
|
||||
</div>
|
||||
<Toggle inputId="switch" v-model="cfgIntel" />
|
||||
<Toggle inputId="switch1" v-model="cfgIntel" />
|
||||
</label>
|
||||
<label
|
||||
id="resolution"
|
||||
@ -88,17 +88,20 @@ const cfgAimeCode = _cfg('aime-code', '00001111222233334444');
|
||||
<label for="ingredient3">fullscreen</label>
|
||||
</div>
|
||||
</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>
|
||||
<Toggle inputId="switch" v-model="cfgAime" />
|
||||
<Toggle inputId="switch2" v-model="cfgAime" />
|
||||
</label>
|
||||
<label class="flex flex-row w-full p-2 items-center">
|
||||
<div class="grow">Aime code</div>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:disabled="store.cfg('aime') === false"
|
||||
:minlength="20"
|
||||
:disabled="store.cfg('aime') !== true"
|
||||
:invalid="
|
||||
store.cfg('aime') === true &&
|
||||
store.cfg('aime-code')?.toString().length !== 20
|
||||
"
|
||||
:maxlength="20"
|
||||
placeholder="00000000000000000000"
|
||||
v-model="cfgAimeCode"
|
||||
|
@ -33,7 +33,6 @@ export const usePkgStore = defineStore('pkg', {
|
||||
const key = ev.payload.pkg;
|
||||
await this.reload(key);
|
||||
this.pkg[key].js.busy = true;
|
||||
console.log('install-start' + key);
|
||||
});
|
||||
|
||||
listen<InstallStatus>('install-end', async (ev) => {
|
||||
@ -41,7 +40,6 @@ export const usePkgStore = defineStore('pkg', {
|
||||
await this.reload(key);
|
||||
await this.reloadProfile();
|
||||
this.pkg[key].js.busy = false;
|
||||
console.log('install-end' + key);
|
||||
});
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user