feat: basic card setup

This commit is contained in:
2025-02-25 19:27:37 +00:00
parent 43fd622322
commit 1586f81152
12 changed files with 140 additions and 69 deletions

View File

@ -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)

View File

@ -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);

View File

@ -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()
}

View File

@ -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![

View File

@ -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)
}

View File

@ -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)]

View File

@ -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);
}

View File

@ -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()
}
}

View File

@ -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))
}

View File

@ -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;
};

View File

@ -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"

View File

@ -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);
});
},