feat: initial support for segatools pkgs
This commit is contained in:
60
README.md
60
README.md
@ -1,30 +1,39 @@
|
||||
## STARTLINER
|
||||
# STARTLINER
|
||||
|
||||
A simple and easy to use launcher, configuration tool and mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (more to come) using [Rainycolor Watercolor](https://rainy.patafour.zip).
|
||||
|
||||
Intended for those who just want a glorified `start.bat` clicker, without VHDs, keychips etc.
|
||||
(for an all-in-one solution, check out the [BlueSteel launcher](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)).
|
||||
|
||||
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
||||
|
||||
### Usage
|
||||
## Features
|
||||
|
||||
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
|
||||
- Segatools configuration
|
||||
- Display configuration with automatic rollback
|
||||
- Support for multiple configurations pointing at the same data
|
||||
|
||||
## Usage
|
||||
|
||||
Download a prebuilt binary from [Modding Re:Fresh](https://discord.gg/jxvzHjjEmc) or build it yourself:
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run tauri dev
|
||||
bun run tauri build
|
||||
```
|
||||
|
||||
Once a profile is set up, it is possible to bypass the GUI:
|
||||
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you can have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used.
|
||||
|
||||
Once a profile has been set up, it is possible to bypass the GUI:
|
||||
|
||||
```sh
|
||||
startliner --start --game ongeki --profile <name>
|
||||
```
|
||||
|
||||
### Package format
|
||||
To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then append `--start --game ongeki --profile <name>` to `Target`.
|
||||
|
||||
## Package format
|
||||
|
||||
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
||||
- A subset of [the simple Rainycolor format](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple) is currently supported.
|
||||
- A subset of the simple BlueSteel Rainycolor format is currently supported. [Full reference (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple)
|
||||
|
||||
```
|
||||
├───app
|
||||
@ -42,35 +51,6 @@ More file overrides may be supported in the future.
|
||||
|
||||
Arbitrary scripts are not supported by design and that will probably never change.
|
||||
|
||||
### Features
|
||||
## See also
|
||||
|
||||
- Clean data modding
|
||||
- Monitor selection
|
||||
- segatools configuration UI
|
||||
- Etc
|
||||
|
||||
### Architecture details
|
||||
|
||||
- Downloaded packages are stored in `%LOCALAPPDATA%\STARTLINER\data\pkg` (or `$XDG_DATA_HOME/startliner/pkg`).
|
||||
- Each profile is associated with a prefix directory `%LOCALAPPDATA%\STARTLINER\data\profile-x` which includes:
|
||||
- `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
|
||||
- logs
|
||||
- Persistent configuration is stored in `%APPDATA%\STARTLINER` (or `$XDG_CONFIG_HOME/startliner`).
|
||||
|
||||
### Todo
|
||||
|
||||
- Auto-updates
|
||||
- CHUNITHM support
|
||||
- segatools as a special package
|
||||
- Progress bars and other GUI sugar
|
||||
- Search bar
|
||||
- Start check
|
||||
|
||||
### Endgame
|
||||
|
||||
- IO DLLs and artemis as special packages
|
||||
- Other arcade games (if there is demand)
|
||||
- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
|
||||
|
12
TODO.md
Normal file
12
TODO.md
Normal file
@ -0,0 +1,12 @@
|
||||
### Short-term
|
||||
|
||||
- CHUNITHM support
|
||||
- Start checks
|
||||
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
|
||||
|
||||
### Long-term
|
||||
|
||||
- Auto-updates
|
||||
- Progress bars and other GUI sugar
|
||||
- IO DLLs and artemis as special packages
|
||||
- Other arcade games (if there is demand)
|
@ -45,11 +45,15 @@ pub async fn kill() -> Result<(), String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
||||
pub async fn install_package(
|
||||
state: State<'_, tokio::sync::Mutex<AppData>>,
|
||||
key: PkgKey,
|
||||
force: bool
|
||||
) -> Result<InstallResult, String> {
|
||||
log::debug!("invoke: install_package({})", key);
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.pkgs.install_package(&key, true, true)
|
||||
appd.pkgs.install_package(&key, force, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::pkg::PkgKey;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Segatools {
|
||||
pub target: PathBuf,
|
||||
pub hook: Option<PkgKey>,
|
||||
pub io: Option<PkgKey>,
|
||||
pub amfs: PathBuf,
|
||||
pub option: PathBuf,
|
||||
pub appdata: PathBuf,
|
||||
@ -15,6 +18,8 @@ impl Default for Segatools {
|
||||
fn default() -> Self {
|
||||
Segatools {
|
||||
target: PathBuf::default(),
|
||||
hook: Some(PkgKey("segatools-mu3hook".to_owned())),
|
||||
io: None,
|
||||
amfs: PathBuf::default(),
|
||||
option: PathBuf::default(),
|
||||
appdata: PathBuf::from("appdata"),
|
||||
|
@ -1,14 +1,16 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use serde::Deserialize;
|
||||
use crate::pkg::PkgKeyVersion;
|
||||
|
||||
// manifest.json
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PackageManifest {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub description: String,
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>
|
||||
}
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
|
||||
#[serde(default)]
|
||||
pub installers: Vec<BTreeMap<String, serde_json::Value>>
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
pub fn segatools_base() -> String {
|
||||
"; mu3io is TBD
|
||||
[mu3io]
|
||||
path=
|
||||
|
||||
[vfd]
|
||||
"[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
enable=1
|
||||
|
@ -57,6 +57,14 @@ impl Segatools {
|
||||
.set("enable", "0");
|
||||
}
|
||||
|
||||
if let Some(io) = &self.io {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", util::pkg_dir().join(io.to_string()).join("mu3io.dll").stringify()?);
|
||||
} else {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", "");
|
||||
}
|
||||
|
||||
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||
|
||||
if !opt_dir_out.exists() {
|
||||
|
@ -3,7 +3,7 @@ use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use tokio::fs;
|
||||
use crate::{model::{local, rainy}, util};
|
||||
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
|
||||
|
||||
// {namespace}-{name}
|
||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
||||
@ -27,8 +27,10 @@ pub struct Package {
|
||||
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Kind {
|
||||
Unchecked,
|
||||
Unsupported,
|
||||
#[default] Mod,
|
||||
Unsupported
|
||||
Hook,
|
||||
IO,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
@ -84,6 +86,7 @@ impl Package {
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let kind = Self::parse_kind(&mft);
|
||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||
|
||||
Ok(Package {
|
||||
@ -94,7 +97,7 @@ impl Package {
|
||||
loc: Some(Local {
|
||||
version: mft.version_number,
|
||||
path: dir.to_owned(),
|
||||
kind: Kind::Mod,
|
||||
kind,
|
||||
dependencies
|
||||
}),
|
||||
rmt: None
|
||||
@ -166,4 +169,25 @@ impl Package {
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn parse_kind(mft: &PackageManifest) -> Kind {
|
||||
if mft.installers.len() == 0 {
|
||||
return Kind::Mod;//Unchecked
|
||||
} else if mft.installers.len() == 1 {
|
||||
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
|
||||
if id == "rainycolor" {
|
||||
return Kind::Mod
|
||||
} else if id == "segatools" {
|
||||
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
|
||||
if module.ends_with("hook") {
|
||||
return Kind::Hook;
|
||||
} else if module.ends_with("io") {
|
||||
return Kind::IO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Kind::Unsupported
|
||||
}
|
||||
}
|
@ -120,13 +120,14 @@ impl PackageStore {
|
||||
}
|
||||
|
||||
pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
|
||||
log::debug!("Installing {}", key);
|
||||
log::info!("installation request: {}/{}/{}", key, force, install_deps);
|
||||
|
||||
let pkg = self.store.get(key)
|
||||
.ok_or_else(|| anyhow!("Attempted to install a nonexistent pkg"))?
|
||||
.clone();
|
||||
|
||||
if pkg.loc.is_some() && !force {
|
||||
log::debug!("installation skipped");
|
||||
return Ok(InstallResult::Ready);
|
||||
}
|
||||
|
||||
@ -152,7 +153,7 @@ impl PackageStore {
|
||||
|
||||
if !zip_path.exists() {
|
||||
self.dlh.download_zip(&zip_path, &pkg)?;
|
||||
log::debug!("Deferring {}", key);
|
||||
log::debug!("deferring {}", key);
|
||||
return Ok(InstallResult::Deferred);
|
||||
}
|
||||
|
||||
@ -170,13 +171,13 @@ impl PackageStore {
|
||||
pkg: key.to_owned()
|
||||
})?;
|
||||
|
||||
log::info!("Installed {}", key);
|
||||
log::info!("installed {}", key);
|
||||
|
||||
Ok(InstallResult::Ready)
|
||||
}
|
||||
|
||||
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> {
|
||||
log::debug!("Will delete {} {}", key, force);
|
||||
log::debug!("will delete {} {}", key, force);
|
||||
|
||||
let pkg = self.store.get_mut(key)
|
||||
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
||||
@ -191,7 +192,7 @@ impl PackageStore {
|
||||
self.app.emit("install-end", Payload {
|
||||
pkg: key.to_owned()
|
||||
})?;
|
||||
log::info!("Deleted {}", key);
|
||||
log::info!("deleted {}", key);
|
||||
}
|
||||
rv
|
||||
} else {
|
||||
@ -216,7 +217,7 @@ impl PackageStore {
|
||||
if path.exists() {
|
||||
tokio::fs::remove_dir_all(path)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||
.map_err(|e| anyhow!("could not delete {}: {}", name, e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -237,6 +238,7 @@ impl PackageStore {
|
||||
// todo case sensitivity for linux
|
||||
Self::clean_up_dir(&path, "app").await?;
|
||||
Self::clean_up_dir(&path, "option").await?;
|
||||
Self::clean_up_dir(&path, "segatools").await?;
|
||||
Self::clean_up_file(&path, "icon.png", true).await?;
|
||||
Self::clean_up_file(&path, "manifest.json", true).await?;
|
||||
Self::clean_up_file(&path, "README.md", true).await?;
|
||||
|
@ -3,7 +3,7 @@ use ongeki::OngekiProfile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use crate::{model::{config::Display, misc::Game}, modules::package::prepare_packages, pkg::PkgKey, util};
|
||||
use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util};
|
||||
|
||||
pub mod ongeki;
|
||||
|
||||
@ -72,14 +72,15 @@ impl AnyProfile {
|
||||
}
|
||||
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
Self::OngekiProfile(_p) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
let info = p.display.line_up()?;
|
||||
let info = _p.display.line_up()?;
|
||||
|
||||
let res = self.line_up_the_rest(pkg_hash).await;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(info) = info {
|
||||
use crate::model::config::Display;
|
||||
if res.is_ok() {
|
||||
Display::wait_for_exit(_app, info);
|
||||
} else {
|
||||
|
@ -88,10 +88,13 @@ impl Profile for OngekiProfile {
|
||||
|
||||
let target_path = PathBuf::from(&self.sgt.target);
|
||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||
let sgt_dir = util::pkg_dir()
|
||||
.join(self.sgt.hook.as_ref().ok_or_else(|| anyhow!("No hook"))?.to_string())
|
||||
.join("segatools");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
game_builder = Command::new(exe_dir.join("inject.exe"));
|
||||
game_builder = Command::new(sgt_dir.join("inject.exe"));
|
||||
amd_builder = Command::new("cmd.exe");
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
@ -99,7 +102,7 @@ impl Profile for OngekiProfile {
|
||||
game_builder = Command::new(&self.wine.runtime);
|
||||
amd_builder = Command::new(&self.wine.runtime);
|
||||
|
||||
game_builder.arg(exe_dir.join("inject.exe"));
|
||||
game_builder.arg(sgt_dir.join("inject.exe"));
|
||||
amd_builder.arg("cmd.exe");
|
||||
}
|
||||
|
||||
@ -109,10 +112,10 @@ impl Profile for OngekiProfile {
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.arg("/C")
|
||||
.arg(&exe_dir.join("inject.exe"))
|
||||
.args([
|
||||
"-d", "-k", "mu3hook.dll",
|
||||
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||
.arg(&sgt_dir.join("inject.exe"))
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join("mu3hook.dll"))
|
||||
.args(["amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||
]);
|
||||
game_builder
|
||||
.env(
|
||||
@ -124,8 +127,9 @@ impl Profile for OngekiProfile {
|
||||
self.config_dir().join("inohara.cfg"),
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join("mu3hook.dll"))
|
||||
.args([
|
||||
"-d", "-k", "mu3hook.dll",
|
||||
"mu3.exe", "-monitor 1",
|
||||
"-screen-width", &self.display.rez.0.to_string(),
|
||||
"-screen-height", &self.display.rez.1.to_string(),
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Ref, computed, onMounted, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Tab from 'primevue/tab';
|
||||
import TabList from 'primevue/tablist';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
@ -21,7 +23,9 @@ const general = useGeneralStore();
|
||||
|
||||
pkg.setupListeners();
|
||||
|
||||
const currentTab = ref('3');
|
||||
const currentTab: Ref<string | number> = ref(3);
|
||||
const searchPkg = ref('');
|
||||
const searchCfg = ref('');
|
||||
|
||||
const isProfileDisabled = computed(() => prf.current === null);
|
||||
|
||||
@ -30,47 +34,81 @@ onMounted(async () => {
|
||||
general.dirs = d as Dirs;
|
||||
});
|
||||
|
||||
const fetch_promise = pkg.fetch();
|
||||
|
||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||
|
||||
if (prf.current !== null) {
|
||||
await pkg.reloadAll();
|
||||
currentTab.value = '0';
|
||||
currentTab.value = 0;
|
||||
}
|
||||
|
||||
fetch_promise.then(async () => {
|
||||
await invoke('install_package', {
|
||||
key: 'segatools-mu3hook',
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<Tabs lazy :value="currentTab" class="h-screen">
|
||||
<Tabs
|
||||
lazy
|
||||
:value="currentTab"
|
||||
v-on:update:value="(value) => (currentTab = value)"
|
||||
class="h-screen"
|
||||
>
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow">
|
||||
<Tab :disabled="isProfileDisabled" value="0"
|
||||
<Tab :disabled="isProfileDisabled" :value="0"
|
||||
><div class="pi pi-list-check"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" value="1"
|
||||
<Tab :disabled="isProfileDisabled" :value="1"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" value="2"
|
||||
<Tab :disabled="isProfileDisabled" :value="2"
|
||||
><div class="pi pi-cog"></div
|
||||
></Tab>
|
||||
<Tab value="3"
|
||||
<Tab :value="3"
|
||||
><div class="pi pi-question-circle"></div
|
||||
></Tab>
|
||||
<div class="grow"></div>
|
||||
<div class="flex" v-if="currentTab !== 3">
|
||||
<InputIcon class="self-center mr-2">
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText
|
||||
v-if="currentTab === 2"
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
v-model="searchCfg"
|
||||
/>
|
||||
<InputText
|
||||
v-else
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
v-model="searchPkg"
|
||||
/>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<StartButton />
|
||||
</TabList>
|
||||
</div>
|
||||
<TabPanels class="w-full grow mt-[3rem]">
|
||||
<TabPanel value="0">
|
||||
<ModList />
|
||||
<TabPanel :value="0">
|
||||
<ModList :search="searchPkg" />
|
||||
</TabPanel>
|
||||
<TabPanel value="1">
|
||||
<ModStore />
|
||||
<TabPanel :value="1">
|
||||
<ModStore :search="searchPkg" />
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<TabPanel :value="2">
|
||||
<OptionList />
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<TabPanel :value="3">
|
||||
<strong>UNDER CONSTRUCTION</strong><br />Some features are
|
||||
missing.<br />Existing features are expected to break
|
||||
sometimes.
|
||||
|
@ -14,7 +14,10 @@ const install = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
||||
await invoke('install_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (props.pkg !== undefined) {
|
||||
|
@ -1,29 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import ModListEntry from './ModListEntry.vue';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { usePkgStore } from '../stores';
|
||||
|
||||
const props = defineProps({
|
||||
search: String,
|
||||
});
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(true);
|
||||
|
||||
const group = () => {
|
||||
const a = Object.assign(
|
||||
{},
|
||||
Object.groupBy(
|
||||
pkg.allLocal
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
p.name
|
||||
.toLowerCase()
|
||||
.includes(props.search.toLowerCase()) ||
|
||||
p.namespace
|
||||
.toLowerCase()
|
||||
.includes(props.search.toLowerCase())
|
||||
)
|
||||
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||
({ namespace }) => namespace
|
||||
)
|
||||
);
|
||||
empty.value = Object.keys(a).length === 0;
|
||||
return a;
|
||||
};
|
||||
|
||||
prf.reload();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
|
||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||
</Fieldset>
|
||||
<div v-if="empty" class="text-3xl">∅</div>
|
||||
</template>
|
||||
|
@ -1,24 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import ModStoreEntry from './ModStoreEntry.vue';
|
||||
import { usePkgStore } from '../stores';
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const empty = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
pkgs.fetch();
|
||||
const props = defineProps({
|
||||
search: String,
|
||||
});
|
||||
|
||||
const list = () => {
|
||||
const res = pkgs.allRemote
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
|
||||
p.namespace
|
||||
.toLowerCase()
|
||||
.includes(props.search.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(props.search.toLowerCase())
|
||||
)
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
||||
empty.value = res.length === 0;
|
||||
return res;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="p in pkgs.allRemote.sort((p1, p2) =>
|
||||
p1.name.localeCompare(p2.name)
|
||||
)"
|
||||
class="flex flex-row"
|
||||
>
|
||||
<div v-for="p in list()" class="flex flex-row">
|
||||
<ModStoreEntry :pkg="p" />
|
||||
</div>
|
||||
<div v-if="empty" class="text-3xl">∅</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -12,8 +12,10 @@ import FilePicker from './FilePicker.vue';
|
||||
import OptionCategory from './OptionCategory.vue';
|
||||
import OptionRow from './OptionRow.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { pkgKey } from '../util';
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
|
||||
const aimeCode = ref('');
|
||||
@ -26,13 +28,6 @@ const displayList: Ref<{ title: string; value: string }[]> = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
// const hookList: Ref<{ title: string; value: string }[]> = ref([
|
||||
// {
|
||||
// title: 'segatools-mu3hook',
|
||||
// value: 'segatools-mu3hook',
|
||||
// },
|
||||
// ]);
|
||||
|
||||
invoke('list_platform_capabilities')
|
||||
.then(async (v: unknown) => {
|
||||
if (Array.isArray(v)) {
|
||||
@ -86,14 +81,18 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<!-- <OptionRow title="mu3hook">
|
||||
<OptionRow title="mu3hook">
|
||||
<Select
|
||||
model-value="segatools-mu3hook"
|
||||
:options="hookList"
|
||||
v-model="prf.current!.sgt.hook"
|
||||
:options="
|
||||
pkg.hooks.map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
})
|
||||
"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow> -->
|
||||
</OptionRow>
|
||||
<OptionRow title="amfs">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
@ -119,6 +118,20 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="mu3io">
|
||||
<Select
|
||||
v-model="prf.current!.sgt.io"
|
||||
placeholder="segatools built-in"
|
||||
:options="[
|
||||
{ title: 'segatools built-in', value: null },
|
||||
...pkg.ios.map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
}),
|
||||
]"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Display">
|
||||
<OptionRow
|
||||
|
@ -31,6 +31,9 @@ const disabledTooltip = computed(() => {
|
||||
if (prf.current?.sgt.amfs.length === 0) {
|
||||
return 'The amfs path must be specified';
|
||||
}
|
||||
if (prf.current?.sgt.hook === null || prf.current?.sgt.hook === undefined) {
|
||||
return 'A segatools hook package is necessary';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
|
@ -14,7 +14,10 @@ const install = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
||||
await invoke('install_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
if (props.pkg !== undefined) {
|
||||
props.pkg.js.busy = false;
|
||||
|
@ -46,8 +46,13 @@ export const usePkgStore = defineStore('pkg', {
|
||||
fromName: (state) => (namespace: string, name: string) =>
|
||||
state.pkg[`${namespace}-${name}`] ?? null,
|
||||
all: (state) => Object.values(state),
|
||||
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
|
||||
allLocal: (state) =>
|
||||
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Mod'),
|
||||
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
||||
hooks: (state) =>
|
||||
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Hook'),
|
||||
ios: (state) =>
|
||||
Object.values(state.pkg).filter((p) => p.loc?.kind === 'IO'),
|
||||
},
|
||||
actions: {
|
||||
setupListeners() {
|
||||
|
@ -7,6 +7,7 @@ export interface Package {
|
||||
version: string;
|
||||
path: string;
|
||||
dependencies: string[];
|
||||
kind: 'Unchecked' | 'Unsupported' | 'Mod' | 'Hook' | 'IO';
|
||||
} | null;
|
||||
rmt: {
|
||||
version: string;
|
||||
@ -28,6 +29,8 @@ export interface ProfileMeta {
|
||||
|
||||
export interface SegatoolsConfig {
|
||||
target: string;
|
||||
hook: string | null;
|
||||
io: string | null;
|
||||
amfs: string;
|
||||
option: string;
|
||||
appdata: string;
|
||||
|
Reference in New Issue
Block a user