Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
890d26e883 | |||
2aff5834b9 | |||
69f2c83109 | |||
dbbd80c6c3 | |||
3479804dca | |||
aaeed669df | |||
7084f40404 | |||
f7e9d7d7db | |||
e87b661f08 | |||
5d2d407659 | |||
795e889bd0 | |||
7071f19877 |
30
CHANGELOG.md
@ -1,3 +1,33 @@
|
|||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
- Chunithm: fixed crash when using mempatcher
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
- Ongeki: cache and mu3.ini config are now split per-profile (requires mu3-mods 3.7+)
|
||||||
|
- Ongeki: added the few config options of mu3.ini that aren't available in TestMenuConfig, or require a restart
|
||||||
|
- Chunithm: added Lumi+ patches
|
||||||
|
- Added support for a non-standard `games` manifest entry intended for local packages
|
||||||
|
- Expected to be an array containing "ongeki", "chunithm" or both
|
||||||
|
- Example: { "games": ["ongeki"] }
|
||||||
|
- Added a button linking to the profile config folder
|
||||||
|
- Fixed the button linking to the data folder showing up when the folder does not exist
|
||||||
|
- Uninstalled tool packages are no longer automatically deselected, as that caused issues
|
||||||
|
|
||||||
|
## 0.11.1
|
||||||
|
|
||||||
|
- Improved help pages
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
- Added help pages
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
|
||||||
|
- Fixed the order of cells in the CHUNITHM keyboard
|
||||||
|
- Fixed numpad bindings with numlock disabled
|
||||||
|
- Disabled primary monitor cleanup when "don't switch primary monitor" is enabled
|
||||||
|
|
||||||
## 0.10.0
|
## 0.10.0
|
||||||
|
|
||||||
- Added a global progress bar
|
- Added a global progress bar
|
||||||
|
20
README.md
@ -1,17 +1,19 @@
|
|||||||
# STARTLINER
|
# STARTLINER
|
||||||
|
|
||||||
A simple and easy to use launcher, configuration tool and mod manager
|
This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
|
||||||
for O.N.G.E.K.I. and CHUNITHM, using [Rainycolor Watercolor](https://rainy.patafour.zip).
|
|
||||||
|
STARTLINER is four things:
|
||||||
|
|
||||||
|
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainy.patafour.zip),
|
||||||
|
- a configuration GUI for segatools,
|
||||||
|
- a glorified `start.bat` clicker, with automatic monitor setup and rollback,
|
||||||
|
- [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).
|
||||||
|
|
||||||
|
STARTLINER's core design principle is to modify, configure and launch games without tampering with them.
|
||||||
|
This makes it possible to keep data cleaner than ever, and to have several configurations pointing at the same data.
|
||||||
|
|
||||||
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
|
|
||||||
- Segatools configuration
|
|
||||||
- Monitor configuration with automatic rollback
|
|
||||||
- Support for multiple configurations pointing at the same data
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
||||||
|
3
public/help-chunithm-server.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
If you're stuck on this screen, restart the game.
|
||||||
|
|
||||||
|
If the problem persists, <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">check your network configuration</a>
|
BIN
public/help-chunithm-server.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
public/help-finale-chunithm.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/help-finale-ongeki.png
Normal file
After Width: | Height: | Size: 12 KiB |
8
public/help-finale.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
You can access this page any time by right-clicking the START button.
|
||||||
|
|
||||||
|
Additional resources:
|
||||||
|
|
||||||
|
- <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">SEGAguide</a>
|
||||||
|
- <a href="https://two-torial.xyz/" target="_blank">two-torial</a>
|
||||||
|
|
||||||
|
## Have fun
|
3
public/help-ongeki-lever.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
You also have to calibrate the lever, or you may get the error 3301.
|
||||||
|
|
||||||
|
Go to lever settings (<span class="bg-black text-white">レバー設定</span>), move the lever to both edges, then press "end" (<span class="bg-black text-white">終了</span>) and "save" (<span class="bg-black text-white">保存する</span>).
|
BIN
public/help-ongeki-lever.png
Normal file
After Width: | Height: | Size: 90 KiB |
3
public/help-ongeki-system-processing.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
You might get stuck on this screen for several minutes. _This is normal_. The game just takes a long time to load data.
|
||||||
|
|
||||||
|
If you install <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, subsequent launches will be much faster.
|
BIN
public/help-ongeki-system-processing.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/help-standard-chunithm.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/help-standard-ongeki.png
Normal file
After Width: | Height: | Size: 129 KiB |
7
public/help-standard.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
You might get stuck on the following screen:
|
||||||
|
|
||||||
|
<div class="p-2 mt-1 mb-1 bg-black text-white">Aグループの基準機から設定を取得</div>
|
||||||
|
|
||||||
|
In which case, you should go to the test menu, and in game settings <span class="bg-black text-white">ゲーム設定</span> switch from "follow the standard machine" <span class="bg-black text-white">基準機に従う</span> to "standard machine" <span class="bg-black text-white">基準機</span>.
|
||||||
|
|
||||||
|
The test menu can be accessed with %TESTMENU%.
|
@ -1,7 +1,8 @@
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
use std::path::Path;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use crate::model::config::GlobalConfig;
|
use crate::model::config::GlobalConfig;
|
||||||
use crate::model::patch::PatchFileVec;
|
use crate::model::patch::{PatchFileVec, PatchList};
|
||||||
use crate::pkg::{Feature, Status};
|
use crate::pkg::{Feature, Status};
|
||||||
use crate::profiles::types::Profile;
|
use crate::profiles::types::Profile;
|
||||||
use crate::{model::misc::Game, pkg::PkgKey};
|
use crate::{model::misc::Game, pkg::PkgKey};
|
||||||
@ -165,4 +166,22 @@ impl AppData {
|
|||||||
panic!("unable to initialize the logger? {:?}", e);
|
panic!("unable to initialize the logger? {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn patches_enabled(&self, game_target: impl AsRef<Path>, amd_target: impl AsRef<Path>) -> Result<Vec<&PatchList>> {
|
||||||
|
let ch1 = sha256::try_digest(game_target.as_ref())?;
|
||||||
|
let ch2 = sha256::try_digest(amd_target.as_ref())?;
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for pfile in &self.patch_vec.0 {
|
||||||
|
for plist in &pfile.0 {
|
||||||
|
let this_hash = plist.sha256.to_ascii_lowercase();
|
||||||
|
log::debug!("checking {}", this_hash);
|
||||||
|
if this_hash == ch1 || this_hash == ch2 {
|
||||||
|
log::debug!("enabling {this_hash}");
|
||||||
|
res.push(plist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,15 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
log::debug!("{}", hash);
|
log::debug!("{}", hash);
|
||||||
|
|
||||||
|
let patches_enabled = appd.patches_enabled(
|
||||||
|
&p.data.sgt.target,
|
||||||
|
&p.data.sgt.target.parent().unwrap().join("amdaemon.exe")
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let info = p.prepare_display()
|
let info = p.prepare_display()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let lineup_res = p.line_up(hash, refresh, &appd.patch_vec).await
|
let lineup_res = p.line_up(hash, refresh, patches_enabled).await
|
||||||
.map_err(|e| e.to_string());
|
.map_err(|e| e.to_string());
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
@ -334,8 +334,8 @@ fn open_window(apph: AppHandle) -> anyhow::Result<()> {
|
|||||||
let config = apph.config().clone();
|
let config = apph.config().clone();
|
||||||
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
|
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||||
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
|
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
|
||||||
.inner_size(900f64, 480f64)
|
.inner_size(900f64, 600f64)
|
||||||
.min_inner_size(900f64, 480f64)
|
.min_inner_size(900f64, 600f64)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
|
use crate::pkg::{PkgKey, PkgKeyVersion};
|
||||||
|
|
||||||
use super::misc::Game;
|
use super::misc::Game;
|
||||||
|
|
||||||
@ -14,7 +14,10 @@ pub struct PackageManifest {
|
|||||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub installers: Vec<BTreeMap<String, serde_json::Value>>
|
pub installers: Vec<BTreeMap<String, serde_json::Value>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub games: Option<Vec<Game>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
|
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
|
||||||
@ -22,6 +25,5 @@ pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
|
|||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct PackageListEntry {
|
pub struct PackageListEntry {
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub status: Status,
|
|
||||||
pub games: Vec<Game>,
|
pub games: Vec<Game>,
|
||||||
}
|
}
|
@ -166,11 +166,26 @@ pub enum Mu3Audio {
|
|||||||
Excl2Ch,
|
Excl2Ch,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Mu3Ini {
|
pub struct Mu3Ini {
|
||||||
pub audio: Option<Mu3Audio>,
|
pub audio: Option<Mu3Audio>,
|
||||||
|
pub sample_rate: i32,
|
||||||
pub blacklist: Option<(i32, i32)>,
|
pub blacklist: Option<(i32, i32)>,
|
||||||
|
pub gp: i32,
|
||||||
|
pub enable_bonus_tracks: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mu3Ini {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
audio: Some(Mu3Audio::Shared),
|
||||||
|
sample_rate: 48_000,
|
||||||
|
blacklist: Some((10000, 19999)),
|
||||||
|
gp: 999,
|
||||||
|
enable_bonus_tracks: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
@ -6,14 +6,14 @@ use tauri::{AppHandle, Listener};
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DisplayInfo {
|
pub struct DisplayInfo {
|
||||||
pub primary: String,
|
pub primary: Option<String>,
|
||||||
pub set: Option<DisplaySet>,
|
pub set: Option<DisplaySet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DisplayInfo {
|
impl Default for DisplayInfo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DisplayInfo {
|
DisplayInfo {
|
||||||
primary: "default".to_owned(),
|
primary: None,
|
||||||
set: query_displays().ok(),
|
set: query_displays().ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ impl Display {
|
|||||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||||
|
|
||||||
let res = DisplayInfo {
|
let res = DisplayInfo {
|
||||||
primary: primary.name().to_owned(),
|
primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
|
||||||
set: Some(display_set.clone()),
|
set: Some(display_set.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,12 +132,14 @@ impl Display {
|
|||||||
let display_set = info.set.as_ref()
|
let display_set = info.set.as_ref()
|
||||||
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
||||||
|
|
||||||
let primary = display_set
|
if let Some(info_primary) = &info.primary {
|
||||||
.displays()
|
let primary = display_set
|
||||||
.find(|display| display.name() == info.primary)
|
.displays()
|
||||||
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
.find(|display| display.name() == info_primary)
|
||||||
|
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
|
||||||
|
|
||||||
primary.set_primary()?;
|
primary.set_primary()?;
|
||||||
|
}
|
||||||
|
|
||||||
display_set.apply()?;
|
display_set.apply()?;
|
||||||
displayz::refresh()?;
|
displayz::refresh()?;
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::model::patch::{Patch, PatchData, PatchFileVec, PatchSelection, PatchSelectionData};
|
use crate::model::patch::{Patch, PatchData, PatchList, PatchSelection, PatchSelectionData};
|
||||||
|
|
||||||
impl PatchSelection {
|
impl PatchSelection {
|
||||||
pub async fn render_to_file(
|
pub async fn render_to_file(
|
||||||
&self,
|
&self,
|
||||||
filename: &str,
|
filename: &str,
|
||||||
patches: &PatchFileVec,
|
patch_lists: &Vec<&PatchList>,
|
||||||
path: impl AsRef<Path>
|
path: impl AsRef<Path>
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut res = "".to_owned();
|
let mut res = "".to_owned();
|
||||||
|
|
||||||
for file in &patches.0 {
|
for list in patch_lists {
|
||||||
for list in &file.0 {
|
if list.filename != filename {
|
||||||
if list.filename != filename {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
for patch in &list.patches {
|
||||||
for patch in &list.patches {
|
if let Some(selection) = self.0.get(&patch.id) {
|
||||||
if let Some(selection) = self.0.get(&patch.id) {
|
res += &Self::render(filename, patch, selection);
|
||||||
res += &Self::render(filename, patch, selection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use ini::Ini;
|
use ini::Ini;
|
||||||
use crate::model::profile::{Mu3Audio, Mu3Ini};
|
use crate::model::profile::{Mu3Audio, Mu3Ini};
|
||||||
|
|
||||||
impl Mu3Ini {
|
impl Mu3Ini {
|
||||||
pub fn line_up(&self, game_path: impl AsRef<Path>) -> Result<()> {
|
pub fn line_up(&self, data_dir: impl AsRef<Path>, cfg_dir: impl AsRef<Path>) -> Result<()> {
|
||||||
let file = game_path.as_ref().join("mu3.ini");
|
let file = cfg_dir.as_ref().join("mu3.ini");
|
||||||
|
|
||||||
if !file.exists() {
|
if !file.exists() {
|
||||||
std::fs::write(&file, "")?;
|
std::fs::write(&file, "")?;
|
||||||
@ -20,9 +20,26 @@ impl Mu3Ini {
|
|||||||
Mu3Audio::Excl2Ch => "2",
|
Mu3Audio::Excl2Ch => "2",
|
||||||
};
|
};
|
||||||
|
|
||||||
ini.with_section(Some("Sound")).set("WasapiExclusive", value);
|
ini.with_section(Some("Sound"))
|
||||||
|
.set("WasapiExclusive", value)
|
||||||
|
.set("SampleRate", self.sample_rate.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(blacklist) = self.blacklist {
|
||||||
|
ini.with_section(Some("Extra"))
|
||||||
|
.set("BlacklistMin", blacklist.0.to_string())
|
||||||
|
.set("BlacklistMax", blacklist.1.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache_path = data_dir.as_ref().join("mu3-mods-cache");
|
||||||
|
let cache_path = cache_path.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Invalid cache path"))?;
|
||||||
|
|
||||||
|
ini.with_section(Some("Extra"))
|
||||||
|
.set("GP", self.gp.to_string())
|
||||||
|
.set("CacheDir", cache_path)
|
||||||
|
.set("UnlockBonusTracks", crate::util::bool_to_01(self.enable_bonus_tracks));
|
||||||
|
|
||||||
ini.write_to_file(file)?;
|
ini.write_to_file(file)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -5,30 +5,30 @@ use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, Confi
|
|||||||
use crate::pkg_store::PackageStore;
|
use crate::pkg_store::PackageStore;
|
||||||
|
|
||||||
impl Segatools {
|
impl Segatools {
|
||||||
pub fn fix(&mut self, store: &PackageStore) {
|
pub fn fix(&mut self, _store: &PackageStore) {
|
||||||
macro_rules! remove_if_nonpresent {
|
// macro_rules! remove_if_nonpresent {
|
||||||
($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
|
// ($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
|
||||||
if let Ok(pkg) = $store.get($key) {
|
// if let Ok(pkg) = $store.get($key) {
|
||||||
if pkg.loc.is_none() {
|
// if pkg.loc.is_none() {
|
||||||
$item = $emptyval;
|
// $item = $emptyval;
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
$item = $emptyval;
|
// $item = $emptyval;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if let Some(key) = &self.hook {
|
// if let Some(key) = &self.hook {
|
||||||
remove_if_nonpresent!(self.hook, key, None, store);
|
// remove_if_nonpresent!(self.hook, key, None, store);
|
||||||
}
|
// }
|
||||||
if let IOSelection::Custom(key) = &self.io2 {
|
// if let IOSelection::Custom(key) = &self.io2 {
|
||||||
remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
|
// remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
|
||||||
}
|
// }
|
||||||
match &self.aime {
|
// match &self.aime {
|
||||||
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
// Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
||||||
Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
// Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
||||||
_ => {},
|
// _ => {},
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
pub fn load_from_ini(&mut self, ini: &Ini, config_dir: impl AsRef<Path>) -> Result<()> {
|
pub fn load_from_ini(&mut self, ini: &Ini, config_dir: impl AsRef<Path>) -> Result<()> {
|
||||||
log::debug!("loading sgt");
|
log::debug!("loading sgt");
|
||||||
|
@ -15,10 +15,14 @@ impl PatchFileVec {
|
|||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
for f in std::fs::read_dir(path)? {
|
for f in std::fs::read_dir(path)? {
|
||||||
let f = f?;
|
let f = f?;
|
||||||
let f = f.path();
|
let f = &f.path();
|
||||||
res.push(
|
match serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?) {
|
||||||
serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?)?
|
Ok(parsed) => res.push(parsed),
|
||||||
);
|
Err(e) => {
|
||||||
|
log::error!("Error parsing {f:?}: {e}");
|
||||||
|
anyhow::bail!("Error parsing {f:?}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(PatchFileVec(res))
|
Ok(PatchFileVec(res))
|
||||||
}
|
}
|
||||||
@ -26,20 +30,21 @@ impl PatchFileVec {
|
|||||||
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
|
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
|
||||||
let checksum = try_digest(target.as_ref())?;
|
let checksum = try_digest(target.as_ref())?;
|
||||||
|
|
||||||
let mut res = Vec::new();
|
let mut res_patches = Vec::new();
|
||||||
for pfile in &self.0 {
|
for pfile in &self.0 {
|
||||||
for plist in &pfile.0 {
|
for plist in &pfile.0 {
|
||||||
log::debug!("checking {}", plist.sha256);
|
let this_hash = plist.sha256.to_ascii_lowercase();
|
||||||
if plist.sha256 == checksum {
|
log::debug!("checking {}", this_hash);
|
||||||
|
if this_hash == checksum {
|
||||||
let mut cloned = plist.clone().patches;
|
let mut cloned = plist.clone().patches;
|
||||||
res.append(&mut cloned);
|
res_patches.append(&mut cloned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.len() == 0 {
|
if res_patches.len() == 0 {
|
||||||
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
|
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res_patches)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -120,7 +120,7 @@ impl Package {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<Package> {
|
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<(Package, Option<Vec<Game>>)> {
|
||||||
let str = fs::read_to_string(dir.join("manifest.json")).await?;
|
let str = fs::read_to_string(dir.join("manifest.json")).await?;
|
||||||
let mft: local::PackageManifest = serde_json::from_str(&str)?;
|
let mft: local::PackageManifest = serde_json::from_str(&str)?;
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ impl Package {
|
|||||||
let status = Self::parse_status(&mft, &dir);
|
let status = Self::parse_status(&mft, &dir);
|
||||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||||
|
|
||||||
Ok(Package {
|
Ok((Package {
|
||||||
namespace: Self::dir_to_namespace(&dir)?,
|
namespace: Self::dir_to_namespace(&dir)?,
|
||||||
name: mft.name.clone(),
|
name: mft.name.clone(),
|
||||||
description: mft.description.clone(),
|
description: mft.description.clone(),
|
||||||
@ -146,7 +146,7 @@ impl Package {
|
|||||||
}),
|
}),
|
||||||
rmt: None,
|
rmt: None,
|
||||||
source
|
source
|
||||||
})
|
}, mft.games))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key(&self) -> PkgKey {
|
pub fn key(&self) -> PkgKey {
|
||||||
|
@ -83,7 +83,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, PackageSource::Rainy).await {
|
if let Ok((pkg, _)) = Package::from_dir(dir, PackageSource::Rainy).await {
|
||||||
self.update_nonremote(key, pkg);
|
self.update_nonremote(key, pkg);
|
||||||
} else {
|
} else {
|
||||||
log::error!("couldn't reload {}", key);
|
log::error!("couldn't reload {}", key);
|
||||||
@ -102,7 +102,13 @@ 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, locally_declared_games))) = res {
|
||||||
|
if let Some(games) = locally_declared_games {
|
||||||
|
self.meta_list.insert(pkg.key(), PackageListEntry {
|
||||||
|
version: pkg.loc.as_ref().unwrap().version.clone(),
|
||||||
|
games
|
||||||
|
});
|
||||||
|
}
|
||||||
self.update_nonremote(pkg.key(), pkg);
|
self.update_nonremote(pkg.key(), pkg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +158,6 @@ impl PackageStore {
|
|||||||
PackageListEntry {
|
PackageListEntry {
|
||||||
// from_rainy() is guaranteed to include rmt
|
// from_rainy() is guaranteed to include rmt
|
||||||
version: r.rmt.as_ref().unwrap().version.clone(),
|
version: r.rmt.as_ref().unwrap().version.clone(),
|
||||||
status: Status::Unchecked,
|
|
||||||
games: vec![ game ],
|
games: vec![ game ],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||||
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
use crate::{model::{misc::Game, patch::{PatchList, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use crate::model::profile::BepInEx;
|
use crate::model::profile::BepInEx;
|
||||||
@ -28,7 +28,7 @@ impl Profile {
|
|||||||
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
|
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
wine: crate::model::profile::Wine::default(),
|
wine: crate::model::profile::Wine::default(),
|
||||||
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None },
|
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini::default()) } else { None },
|
||||||
keyboard:
|
keyboard:
|
||||||
if meta.game == Game::Ongeki {
|
if meta.game == Game::Ongeki {
|
||||||
Some(Keyboard::Ongeki(OngekiKeyboard::default()))
|
Some(Keyboard::Ongeki(OngekiKeyboard::default()))
|
||||||
@ -43,6 +43,12 @@ impl Profile {
|
|||||||
std::fs::create_dir_all(p.config_dir())?;
|
std::fs::create_dir_all(p.config_dir())?;
|
||||||
std::fs::create_dir_all(p.data_dir())?;
|
std::fs::create_dir_all(p.data_dir())?;
|
||||||
|
|
||||||
|
if meta.game == Game::Ongeki {
|
||||||
|
if let Err(e) = Self::load_existing_mu3_ini(&p.data, &p.meta) {
|
||||||
|
log::error!("unable to load existing mu3.ini: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match meta.game {
|
match meta.game {
|
||||||
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
|
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
|
||||||
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
|
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
|
||||||
@ -67,6 +73,18 @@ impl Profile {
|
|||||||
data.sgt.io2 = IOSelection::Custom(io);
|
data.sgt.io2 = IOSelection::Custom(io);
|
||||||
data.sgt.io = None;
|
data.sgt.io = None;
|
||||||
}
|
}
|
||||||
|
if let Some(ini) = &mut data.mu3_ini {
|
||||||
|
if ini.audio.is_none() {
|
||||||
|
ini.audio = Some(crate::model::profile::Mu3Audio::Shared);
|
||||||
|
}
|
||||||
|
if ini.blacklist.is_none() {
|
||||||
|
ini.blacklist = Some((10000, 19999));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.mu3_ini = Some(Mu3Ini::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::load_existing_mu3_ini(&data, &ProfileMeta { game, name: name.clone() })?;
|
||||||
}
|
}
|
||||||
if game == Game::Chunithm {
|
if game == Game::Chunithm {
|
||||||
if data.keyboard.is_none() {
|
if data.keyboard.is_none() {
|
||||||
@ -168,7 +186,7 @@ impl Profile {
|
|||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
|
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patchlists_enabled: Vec<&PatchList>) -> Result<()> {
|
||||||
if !self.data_dir().exists() {
|
if !self.data_dir().exists() {
|
||||||
tokio::fs::create_dir(self.data_dir()).await?;
|
tokio::fs::create_dir(self.data_dir()).await?;
|
||||||
}
|
}
|
||||||
@ -203,13 +221,13 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mu3ini) = &self.data.mu3_ini {
|
if let Some(mu3ini) = &self.data.mu3_ini {
|
||||||
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?;
|
mu3ini.line_up(&self.data_dir(), &self.config_dir())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(patches) = &self.data.patches {
|
if let Some(patches) = &self.data.patches {
|
||||||
futures::try_join!(
|
futures::try_join!(
|
||||||
patches.render_to_file("amdaemon.exe", patch_files, self.data_dir().join("patch-amd.mph")),
|
patches.render_to_file("amdaemon.exe", &patchlists_enabled, self.data_dir().join("patch-amd.mph")),
|
||||||
patches.render_to_file("chusanApp.exe", patch_files, self.data_dir().join("patch-game.mph"))
|
patches.render_to_file("chusanApp.exe", &patchlists_enabled, self.data_dir().join("patch-game.mph"))
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +301,14 @@ impl Profile {
|
|||||||
"ONGEKI_LANG_PATH",
|
"ONGEKI_LANG_PATH",
|
||||||
self.data_dir().join("lang"),
|
self.data_dir().join("lang"),
|
||||||
)
|
)
|
||||||
|
.env(
|
||||||
|
"MU3_MODS_CONFIG_PATH",
|
||||||
|
self.config_dir().join("mu3.ini"),
|
||||||
|
)
|
||||||
|
.env(
|
||||||
|
"STARTLINER",
|
||||||
|
"1"
|
||||||
|
)
|
||||||
.current_dir(&exe_dir)
|
.current_dir(&exe_dir)
|
||||||
.raw_arg("-d")
|
.raw_arg("-d")
|
||||||
.raw_arg("-k")
|
.raw_arg("-k")
|
||||||
@ -403,6 +429,17 @@ impl Profile {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> {
|
||||||
|
let mu3_ini_target_path = data.sgt.target.parent().ok_or_else(|| anyhow!("invalid target directory"))?.join("mu3.ini");
|
||||||
|
let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini");
|
||||||
|
log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path);
|
||||||
|
if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() {
|
||||||
|
std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?;
|
||||||
|
log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfilePaths for Profile {
|
impl ProfilePaths for Profile {
|
||||||
|
@ -199,7 +199,7 @@ pub fn create_shortcut(
|
|||||||
obj.SetDescription(&format!("{} – {} (STARTLINER)", &meta.game.print(), &meta.name))?;
|
obj.SetDescription(&format!("{} – {} (STARTLINER)", &meta.game.print(), &meta.name))?;
|
||||||
obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?;
|
obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?;
|
||||||
obj.SetIconLocation(
|
obj.SetIconLocation(
|
||||||
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
|
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
|
||||||
0
|
0
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -1,4 +1,154 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
filename: 'chusanApp.exe',
|
||||||
|
version: '2.26.00',
|
||||||
|
sha256: 'AD2DCC02CE52B3FFF24A2919F8617854581DD2E2C0378EA13D84438FCCA2D522',
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
id: 'standard-shared-audio',
|
||||||
|
name: "Force shared audio mode, system audio sample rate must be 48000Hz",
|
||||||
|
tooltip: "Improves compatibility, but may increase latency",
|
||||||
|
patches: [
|
||||||
|
{offset: 0xF233DA, off: [0x01], on: [0x00]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-2ch',
|
||||||
|
name: "Force 2 channel audio output",
|
||||||
|
tooltip: "May cause bass overload",
|
||||||
|
patches: [
|
||||||
|
{offset: 0xF234B1, off: [0x75, 0x3f], on: [0x90, 0x90]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-song-timer',
|
||||||
|
name: "Disable song select timer",
|
||||||
|
patches: [
|
||||||
|
{offset: 0xA03916, off: [0x74], on: [0xeb]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-map-timer',
|
||||||
|
name: "Map selection timer",
|
||||||
|
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
|
||||||
|
type: "number",
|
||||||
|
offset: 0x965B37,
|
||||||
|
default: 30,
|
||||||
|
size: 1,
|
||||||
|
min: -128,
|
||||||
|
max: 127,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-ticket-timer',
|
||||||
|
name: "Ticket selection timer",
|
||||||
|
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
|
||||||
|
type: "number",
|
||||||
|
offset: 0x9592C2,
|
||||||
|
default: 60,
|
||||||
|
size: 1,
|
||||||
|
min: -128,
|
||||||
|
max: 127,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-course-timer',
|
||||||
|
name: "Course selection timer",
|
||||||
|
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
|
||||||
|
type: "number",
|
||||||
|
offset: 0xA0EADB,
|
||||||
|
default: 30,
|
||||||
|
size: 1,
|
||||||
|
min: -128,
|
||||||
|
max: 127,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-unlimited-tracks',
|
||||||
|
name: "Unlimited maximum tracks",
|
||||||
|
tooltip: "Must check to play more than 7 tracks per credit",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x71E2E0, off: [0xf0], on: [0xc0]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-maximum-tracks',
|
||||||
|
type: "number",
|
||||||
|
name: "Maximum tracks",
|
||||||
|
offset: 0x3980C1,
|
||||||
|
default: 3,
|
||||||
|
size: 1,
|
||||||
|
min: 3,
|
||||||
|
max: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-no-encryption',
|
||||||
|
name: "No encryption",
|
||||||
|
tooltip: "Will also disable TLS",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x1DE29E8, off: [0xE1], on: [0x00]},
|
||||||
|
{offset: 0x1DE29EC, off: [0xE1], on: [0x00]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-no-tls',
|
||||||
|
name: "No TLS",
|
||||||
|
tooltip: "Title server workaround",
|
||||||
|
patches: [
|
||||||
|
{offset: 0xF06447, off: [0x80], on: [0x00]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-head-to-head',
|
||||||
|
name: "Patch for head-to-head play",
|
||||||
|
tooltip: "Fix infinite sync while trying to connect to head to head play",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x6533A3, off: [0x01], on: [0x00]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-bypass-1080p',
|
||||||
|
name: "Bypass 1080p monitor check",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x1CCBF, off: [0x81, 0xbc, 0x24, 0xb8, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x75, 0x1f, 0x81, 0xbc, 0x24, 0xbc, 0x02, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x75, 0x12], on: [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-bypass-120hz',
|
||||||
|
name: "Bypass 120Hz monitor check",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x1CCB1, off: [0x85, 0xc0], on: [0xeb, 0x30]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-force-free-play-text',
|
||||||
|
name: "Force FREE PLAY credit text",
|
||||||
|
tooltip: "Replaces the credit count with FREE PLAY",
|
||||||
|
patches: [
|
||||||
|
{offset: 0x3875A4, off: [0x3c, 0x01], on: [0x38, 0xc0]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: 'amdaemon.exe',
|
||||||
|
version: '2.25.00',
|
||||||
|
sha256: '00FB867D1EE821033101B8773FAC116A45DF1939D23C38E9DAFC9B86CD5A3777',
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
id: 'standard-localhost',
|
||||||
|
name: "Allow 127.0.0.1/localhost as the network server",
|
||||||
|
patches: [
|
||||||
|
{ offset: 0x6E28A4, off: [0x31, 0x32, 0x37, 0x2F], on: [0x30, 0x2F, 0x38, 0x00] },
|
||||||
|
{ offset: 0x3C94C4, off: [0xFF, 0x15, 0xC6, 0x2F, 0x1B, 0x00, 0x8B], on: [0x33, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-credit-freeze',
|
||||||
|
name: "Infinite credits",
|
||||||
|
patches: [
|
||||||
|
{ offset: 0x2BBBC8, off: [0x28], on: [0x08] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
filename: 'chusanApp.exe',
|
filename: 'chusanApp.exe',
|
||||||
version: '2.30.00',
|
version: '2.30.00',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "STARTLINER",
|
"productName": "STARTLINER",
|
||||||
"version": "0.10.0",
|
"version": "0.12.1",
|
||||||
"identifier": "zip.patafour.startliner",
|
"identifier": "zip.patafour.startliner",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
@ -65,7 +65,7 @@ const filePick = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
|
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
|
||||||
<div v-else>
|
<div class="primitive-base" v-else>
|
||||||
<Button
|
<Button
|
||||||
v-if="exists"
|
v-if="exists"
|
||||||
icon="pi pi-pen-to-square"
|
icon="pi pi-pen-to-square"
|
||||||
@ -102,12 +102,20 @@ const filePick = async () => {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10vh;
|
top: 50%;
|
||||||
left: 10vw;
|
left: 50%;
|
||||||
height: 80vh;
|
height: 500px;
|
||||||
width: 80vw;
|
width: 800px;
|
||||||
|
margin-left: -400px;
|
||||||
|
margin-top: -250px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
background-color: #151515;
|
background-color: #151515;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primitive-base ::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -16,7 +16,7 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
|
|||||||
O.N.G.E.K.I. and CHUNITHM.
|
O.N.G.E.K.I. and CHUNITHM.
|
||||||
<h1>Changelog</h1>
|
<h1>Changelog</h1>
|
||||||
<ScrollPanel style="height: 200px">
|
<ScrollPanel style="height: 200px">
|
||||||
<div class="changelog">
|
<div class="markdown">
|
||||||
<vue-markdown-it
|
<vue-markdown-it
|
||||||
:source="changelog"
|
:source="changelog"
|
||||||
:options="{ typographer: true, breaks: true }"
|
:options="{ typographer: true, breaks: true }"
|
||||||
@ -44,13 +44,20 @@ h1 {
|
|||||||
font-size: 1.7rem;
|
font-size: 1.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelog h2 {
|
.markdown h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
.changelog ul {
|
.markdown ul {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
}
|
}
|
||||||
.changelog li {
|
.markdown li {
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
}
|
}
|
||||||
|
.markdown a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import { fromKeycode, toKeycode } from '../keyboard';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
import { OngekiButtons } from '../types';
|
import { OngekiButtons } from '../types';
|
||||||
|
|
||||||
@ -15,9 +16,51 @@ const handleKey = (
|
|||||||
) => {
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const keycode = toKeycode(event.code);
|
let keycode = toKeycode(event.code);
|
||||||
|
|
||||||
if (keycode !== null && button !== undefined) {
|
if (keycode !== null && button !== undefined) {
|
||||||
const data = prf.current!.data.keyboard!.data as any;
|
const data = prf.current!.data.keyboard!.data as any;
|
||||||
|
|
||||||
|
if (event.getModifierState('NumLock') === false) {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'NumpadDecimal':
|
||||||
|
keycode = toKeycode('Delete');
|
||||||
|
break;
|
||||||
|
case 'Numpad0':
|
||||||
|
keycode = toKeycode('Insert');
|
||||||
|
break;
|
||||||
|
case 'Numpad1':
|
||||||
|
keycode = toKeycode('End');
|
||||||
|
break;
|
||||||
|
case 'Numpad2':
|
||||||
|
keycode = toKeycode('ArrowDown');
|
||||||
|
break;
|
||||||
|
case 'Numpad3':
|
||||||
|
keycode = toKeycode('PageDown');
|
||||||
|
break;
|
||||||
|
case 'Numpad4':
|
||||||
|
keycode = toKeycode('ArrowLeft');
|
||||||
|
break;
|
||||||
|
case 'Numpad5':
|
||||||
|
keycode = toKeycode('Clear');
|
||||||
|
break;
|
||||||
|
case 'Numpad6':
|
||||||
|
keycode = toKeycode('ArrowRight');
|
||||||
|
break;
|
||||||
|
case 'Numpad7':
|
||||||
|
keycode = toKeycode('Home');
|
||||||
|
break;
|
||||||
|
case 'Numpad8':
|
||||||
|
keycode = toKeycode('ArrowUp');
|
||||||
|
break;
|
||||||
|
case 'Numpad9':
|
||||||
|
keycode = toKeycode('PageUp');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (index !== undefined) {
|
if (index !== undefined) {
|
||||||
data[button][index] = keycode;
|
data[button][index] = keycode;
|
||||||
} else {
|
} else {
|
||||||
@ -75,7 +118,7 @@ const handleMouse = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getKey = (key: keyof OngekiButtons, index?: number) =>
|
const getKey = (key: keyof OngekiButtons, index?: number): any =>
|
||||||
computed(() => {
|
computed(() => {
|
||||||
const data = prf.current!.data.keyboard?.data as any;
|
const data = prf.current!.data.keyboard?.data as any;
|
||||||
const keycode =
|
const keycode =
|
||||||
@ -85,147 +128,45 @@ const getKey = (key: keyof OngekiButtons, index?: number) =>
|
|||||||
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '–';
|
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '–';
|
||||||
});
|
});
|
||||||
|
|
||||||
const KEY_MAP: { [key: number]: string } = {
|
const props = defineProps({
|
||||||
1: 'M1',
|
|
||||||
2: 'M2',
|
|
||||||
4: 'M3',
|
|
||||||
5: 'M4',
|
|
||||||
6: 'M5',
|
|
||||||
8: 'Backspace',
|
|
||||||
9: 'Tab',
|
|
||||||
13: 'Enter',
|
|
||||||
19: 'Pause',
|
|
||||||
20: 'CapsLock',
|
|
||||||
27: 'Escape',
|
|
||||||
32: 'Space',
|
|
||||||
33: 'PageUp',
|
|
||||||
34: 'PageDown',
|
|
||||||
35: 'End',
|
|
||||||
36: 'Home',
|
|
||||||
37: 'ArrowLeft',
|
|
||||||
38: 'ArrowUp',
|
|
||||||
39: 'ArrowRight',
|
|
||||||
40: 'ArrowDown',
|
|
||||||
45: 'Insert',
|
|
||||||
46: 'Delete',
|
|
||||||
48: 'Digit0',
|
|
||||||
49: 'Digit1',
|
|
||||||
50: 'Digit2',
|
|
||||||
51: 'Digit3',
|
|
||||||
52: 'Digit4',
|
|
||||||
53: 'Digit5',
|
|
||||||
54: 'Digit6',
|
|
||||||
55: 'Digit7',
|
|
||||||
56: 'Digit8',
|
|
||||||
57: 'Digit9',
|
|
||||||
65: 'KeyA',
|
|
||||||
66: 'KeyB',
|
|
||||||
67: 'KeyC',
|
|
||||||
68: 'KeyD',
|
|
||||||
69: 'KeyE',
|
|
||||||
70: 'KeyF',
|
|
||||||
71: 'KeyG',
|
|
||||||
72: 'KeyH',
|
|
||||||
73: 'KeyI',
|
|
||||||
74: 'KeyJ',
|
|
||||||
75: 'KeyK',
|
|
||||||
76: 'KeyL',
|
|
||||||
77: 'KeyM',
|
|
||||||
78: 'KeyN',
|
|
||||||
79: 'KeyO',
|
|
||||||
80: 'KeyP',
|
|
||||||
81: 'KeyQ',
|
|
||||||
82: 'KeyR',
|
|
||||||
83: 'KeyS',
|
|
||||||
84: 'KeyT',
|
|
||||||
85: 'KeyU',
|
|
||||||
86: 'KeyV',
|
|
||||||
87: 'KeyW',
|
|
||||||
88: 'KeyX',
|
|
||||||
89: 'KeyY',
|
|
||||||
90: 'KeyZ',
|
|
||||||
91: 'MetaLeft',
|
|
||||||
92: 'MetaRight',
|
|
||||||
93: 'ContextMenu',
|
|
||||||
96: 'Numpad0',
|
|
||||||
97: 'Numpad1',
|
|
||||||
98: 'Numpad2',
|
|
||||||
99: 'Numpad3',
|
|
||||||
100: 'Numpad4',
|
|
||||||
101: 'Numpad5',
|
|
||||||
102: 'Numpad6',
|
|
||||||
103: 'Numpad7',
|
|
||||||
104: 'Numpad8',
|
|
||||||
105: 'Numpad9',
|
|
||||||
106: 'NumpadMultiply',
|
|
||||||
107: 'NumpadAdd',
|
|
||||||
109: 'NumpadSubtract',
|
|
||||||
110: 'NumpadDecimal',
|
|
||||||
111: 'NumpadDivide',
|
|
||||||
112: 'F1',
|
|
||||||
113: 'F2',
|
|
||||||
114: 'F3',
|
|
||||||
115: 'F4',
|
|
||||||
116: 'F5',
|
|
||||||
117: 'F6',
|
|
||||||
118: 'F7',
|
|
||||||
119: 'F8',
|
|
||||||
120: 'F9',
|
|
||||||
121: 'F10',
|
|
||||||
122: 'F11',
|
|
||||||
123: 'F12',
|
|
||||||
144: 'NumLock',
|
|
||||||
145: 'ScrollLock',
|
|
||||||
160: 'ShiftLeft',
|
|
||||||
161: 'ShiftRight',
|
|
||||||
162: 'ControlLeft',
|
|
||||||
163: 'ControlRight',
|
|
||||||
164: 'AltLeft',
|
|
||||||
165: 'AltRight',
|
|
||||||
186: 'Semicolon',
|
|
||||||
187: 'Equal',
|
|
||||||
188: 'Comma',
|
|
||||||
189: 'Minus',
|
|
||||||
190: 'Period',
|
|
||||||
191: 'Slash',
|
|
||||||
192: 'Backquote',
|
|
||||||
219: 'BracketLeft',
|
|
||||||
220: 'Backslash',
|
|
||||||
221: 'BracketRight',
|
|
||||||
222: 'Quote',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fromKeycode = (keyCode: number): string | null => {
|
|
||||||
return KEY_MAP[keyCode] ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toKeycode = (key: string): number | null => {
|
|
||||||
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
|
|
||||||
return res ? parseInt(res) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
verySmall: Boolean,
|
|
||||||
tall: Boolean,
|
tall: Boolean,
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
button: String,
|
button: String,
|
||||||
color: String,
|
color: String,
|
||||||
index: Number,
|
index: Number,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modelValue = computed(() => {
|
||||||
|
return getKey(props.button as keyof OngekiButtons, props.index).value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fontSize = computed(() => {
|
||||||
|
if (!props.small) {
|
||||||
|
return '1rem';
|
||||||
|
}
|
||||||
|
const len = modelValue.value.length;
|
||||||
|
if (len < 5) {
|
||||||
|
return '1rem';
|
||||||
|
}
|
||||||
|
if (len < 7) {
|
||||||
|
return '0.75rem';
|
||||||
|
}
|
||||||
|
return '0.5rem';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<InputText
|
<InputText
|
||||||
:style="{
|
:style="{
|
||||||
width: small ? '3em' : '5em',
|
width: small ? '2.8rem' : '5rem',
|
||||||
height: small ? '3em' : tall ? '10em' : '5em',
|
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||||
fontSize: small ? '0.9em' : '1em',
|
fontSize,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
}"
|
}"
|
||||||
unstyled
|
unstyled
|
||||||
class="text-center buttoninputtext"
|
class="text-center buttoninputtext"
|
||||||
v-tooltip="tooltip"
|
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||||
@contextmenu.prevent="() => {}"
|
@contextmenu.prevent="() => {}"
|
||||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||||
@mousedown="
|
@mousedown="
|
||||||
@ -233,7 +174,7 @@ defineProps({
|
|||||||
handleMouse(button as keyof OngekiButtons, ev, index)
|
handleMouse(button as keyof OngekiButtons, ev, index)
|
||||||
"
|
"
|
||||||
@focusout="() => (hasClickedM1Once = false)"
|
@focusout="() => (hasClickedM1Once = false)"
|
||||||
:model-value="getKey(button as keyof OngekiButtons, index) as any"
|
:model-value="modelValue"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -241,5 +182,7 @@ defineProps({
|
|||||||
.buttoninputtext {
|
.buttoninputtext {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(200, 200, 200, 0.3);
|
border: 1px solid rgba(200, 200, 200, 0.3);
|
||||||
|
overflow: scroll !important;
|
||||||
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -38,7 +38,7 @@ const iconSrc = computed(() => {
|
|||||||
<label class="m-3 align-middle text grow z-5 h-50px">
|
<label class="m-3 align-middle text grow z-5 h-50px">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-lg">
|
<span class="text-lg">
|
||||||
{{ pkg?.name ?? 'Untitled' }}
|
{{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="pkg?.rmt?.deprecated"
|
v-if="pkg?.rmt?.deprecated"
|
||||||
|
175
src/components/Onboarding.vue
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ComputedRef, computed, onMounted, ref } from 'vue';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Carousel from 'primevue/carousel';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import { fromKeycode } from '../keyboard';
|
||||||
|
import { useClientStore, usePrfStore } from '../stores';
|
||||||
|
import { prettyPrint } from '../util';
|
||||||
|
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
const client = useClientStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
firstTime: Boolean,
|
||||||
|
onFinish: Function,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Datum {
|
||||||
|
text: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = computed(() => prf.current?.meta.game);
|
||||||
|
|
||||||
|
const processText = (s: string) => {
|
||||||
|
if (prf.current!.data.keyboard?.data.enabled) {
|
||||||
|
const testKey = prf.current!.data.keyboard?.data.test;
|
||||||
|
const readable = fromKeycode(testKey);
|
||||||
|
if (readable !== null) {
|
||||||
|
return s.replace(
|
||||||
|
'%TESTMENU%',
|
||||||
|
`${readable} or a button on the back of the controller`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.replace('%TESTMENU%', 'a button on the back of the controller');
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async (title: string) => {
|
||||||
|
return {
|
||||||
|
text: await (await fetch(`/help-${title}.md`)).text(),
|
||||||
|
image: `help-${title}.png`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let systemProcessing: Datum;
|
||||||
|
let standardOngeki: Datum;
|
||||||
|
let standardChunithm: Datum;
|
||||||
|
let lever: Datum;
|
||||||
|
let server: Datum;
|
||||||
|
let finaleOngeki: Datum;
|
||||||
|
let finaleChunithm: Datum;
|
||||||
|
|
||||||
|
const data: ComputedRef<Datum[]> = computed(() => {
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
switch (prf.current?.meta.game) {
|
||||||
|
case 'ongeki':
|
||||||
|
res.push(systemProcessing);
|
||||||
|
res.push(standardOngeki);
|
||||||
|
res.push(lever);
|
||||||
|
res.push(finaleOngeki);
|
||||||
|
break;
|
||||||
|
case 'chunithm':
|
||||||
|
res.push(standardChunithm);
|
||||||
|
res.push(server);
|
||||||
|
res.push(finaleChunithm);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
[standardOngeki, systemProcessing, lever, server, finaleOngeki] =
|
||||||
|
await Promise.all([
|
||||||
|
loadPage('standard'),
|
||||||
|
loadPage('ongeki-system-processing'),
|
||||||
|
loadPage('ongeki-lever'),
|
||||||
|
loadPage('chunithm-server'),
|
||||||
|
loadPage('finale'),
|
||||||
|
]);
|
||||||
|
standardOngeki = {
|
||||||
|
...standardOngeki,
|
||||||
|
image: '/help-standard-ongeki.png',
|
||||||
|
};
|
||||||
|
standardChunithm = {
|
||||||
|
...standardOngeki,
|
||||||
|
image: '/help-standard-chunithm.png',
|
||||||
|
};
|
||||||
|
finaleOngeki = {
|
||||||
|
...finaleOngeki,
|
||||||
|
image: '/help-finale-ongeki.png',
|
||||||
|
};
|
||||||
|
finaleChunithm = {
|
||||||
|
...finaleOngeki,
|
||||||
|
image: '/help-finale-chunithm.png',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const counter = ref(0);
|
||||||
|
|
||||||
|
const exitLabel = computed(() => {
|
||||||
|
return props.firstTime === true && counter.value < data.value.length - 1
|
||||||
|
? 'Skip'
|
||||||
|
: 'Close';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
modal
|
||||||
|
:visible="visible"
|
||||||
|
:closable="false"
|
||||||
|
:header="
|
||||||
|
firstTime
|
||||||
|
? `It looks like you're running ${game ? prettyPrint(game) : '<game>'} for the first time`
|
||||||
|
: `${game ? prettyPrint(game) : '<game>'} help`
|
||||||
|
"
|
||||||
|
:style="{ width: '760px', scale: client.scaleValue }"
|
||||||
|
>
|
||||||
|
<Carousel
|
||||||
|
:value="data"
|
||||||
|
:num-visible="1"
|
||||||
|
:num-scroll="1"
|
||||||
|
:page="counter"
|
||||||
|
v-on:update:page="(p) => (counter = p)"
|
||||||
|
>
|
||||||
|
<template #item="slotProps">
|
||||||
|
<div class="md-container markdown">
|
||||||
|
<vue-markdown-it
|
||||||
|
:source="processText(slotProps.data?.text)"
|
||||||
|
:options="{
|
||||||
|
typographer: true,
|
||||||
|
breaks: true,
|
||||||
|
html: true,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border border-surface-200 dark:border-surface-700 rounded m-2"
|
||||||
|
>
|
||||||
|
<img :src="slotProps.data.image" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Carousel>
|
||||||
|
<div style="width: 100%; text-align: center">
|
||||||
|
<Button
|
||||||
|
v-if="counter < data.length - 1"
|
||||||
|
class="m-auto mr-4"
|
||||||
|
label="Next"
|
||||||
|
@click="() => (counter += 1)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="m-auto"
|
||||||
|
:label="exitLabel"
|
||||||
|
@click="() => onFinish && onFinish()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.p-dialog ::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-container {
|
||||||
|
height: 9.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import InputNumber from 'primevue/inputnumber';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import FileEditor from './FileEditor.vue';
|
import FileEditor from './FileEditor.vue';
|
||||||
@ -16,53 +17,35 @@ import { usePrfStore } from '../stores';
|
|||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
|
||||||
const audioModel = computed({
|
const blacklistMinModel = computed({
|
||||||
get() {
|
get() {
|
||||||
return prf.current?.data.mu3_ini?.audio ?? null;
|
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
||||||
},
|
return null;
|
||||||
set(value: 'Shared' | 'Excl6Ch' | 'Excl2Ch') {
|
|
||||||
if (prf.current!.data.mu3_ini === undefined) {
|
|
||||||
prf.current!.data.mu3_ini = {};
|
|
||||||
}
|
}
|
||||||
prf.current!.data.mu3_ini!.audio = value;
|
return prf.current?.data.mu3_ini?.blacklist[0];
|
||||||
|
},
|
||||||
|
set(value: number) {
|
||||||
|
prf.current!.data.mu3_ini!.blacklist = [
|
||||||
|
value,
|
||||||
|
prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
|
||||||
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// const blacklistMinModel = computed({
|
const blacklistMaxModel = computed({
|
||||||
// get() {
|
get() {
|
||||||
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
||||||
// return null;
|
return null;
|
||||||
// }
|
}
|
||||||
// return prf.current?.data.mu3_ini?.blacklist[0];
|
return prf.current?.data.mu3_ini.blacklist[1];
|
||||||
// },
|
},
|
||||||
// set(value: number) {
|
set(value: number) {
|
||||||
// if (prf.current!.data.mu3_ini === undefined) {
|
prf.current!.data.mu3_ini!.blacklist = [
|
||||||
// prf.current!.data.mu3_ini = {};
|
prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
|
||||||
// }
|
value,
|
||||||
// prf.current!.data.mu3_ini!.blacklist = [
|
];
|
||||||
// value,
|
},
|
||||||
// prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
|
});
|
||||||
// ];
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const blacklistMaxModel = computed({
|
|
||||||
// get() {
|
|
||||||
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// return prf.current?.data.mu3_ini?.blacklist[1];
|
|
||||||
// },
|
|
||||||
// set(value: number) {
|
|
||||||
// if (prf.current!.data.mu3_ini === undefined) {
|
|
||||||
// prf.current!.data.mu3_ini = {};
|
|
||||||
// }
|
|
||||||
// prf.current!.data.mu3_ini!.blacklist = [
|
|
||||||
// prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
|
|
||||||
// value,
|
|
||||||
// ];
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
prf.reload();
|
prf.reload();
|
||||||
</script>
|
</script>
|
||||||
@ -102,34 +85,59 @@ prf.reload();
|
|||||||
|
|
||||||
<OptionRow
|
<OptionRow
|
||||||
title="Audio mode"
|
title="Audio mode"
|
||||||
tooltip="Exclusive 2-channel mode requires a patch"
|
tooltip="Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio"
|
||||||
>
|
>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="audioModel"
|
v-model="prf.current!.data.mu3_ini!.audio"
|
||||||
:options="[
|
:options="[
|
||||||
{ title: 'Shared', value: 'Shared' },
|
{ title: 'Shared', value: 'Shared' },
|
||||||
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
|
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
|
||||||
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
|
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
|
||||||
]"
|
]"
|
||||||
:allow-empty="true"
|
:allow-empty="false"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
/></OptionRow>
|
/></OptionRow>
|
||||||
|
|
||||||
<!-- <OptionRow
|
<OptionRow
|
||||||
|
title="Sample rate"
|
||||||
|
v-if="
|
||||||
|
prf.current?.data.mods.includes(
|
||||||
|
'7EVENDAYSHOLIDAYS-ExclusiveAudio'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SelectButton
|
||||||
|
v-model="prf.current!.data.mu3_ini!.sample_rate"
|
||||||
|
:disabled="prf.current!.data.mu3_ini!.audio === 'Shared'"
|
||||||
|
:options="[
|
||||||
|
{ title: '44.1KHz', value: 44100 },
|
||||||
|
{ title: '48KHz', value: 48000 },
|
||||||
|
{ title: '96KHz', value: 96000 },
|
||||||
|
{ title: '192KHz', value: 192000 },
|
||||||
|
]"
|
||||||
|
:allow-empty="false"
|
||||||
|
option-label="title"
|
||||||
|
option-value="value"
|
||||||
|
/></OptionRow>
|
||||||
|
|
||||||
|
<OptionRow
|
||||||
|
v-if="
|
||||||
|
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-Blacklist')
|
||||||
|
"
|
||||||
class="number-input"
|
class="number-input"
|
||||||
title="Song ID Blacklist"
|
title="Song ID Blacklist"
|
||||||
tooltip="Requires a patch"
|
tooltip="Scores on charts within this ID range will not be saved nor uploaded"
|
||||||
><InputNumber
|
><InputNumber
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:min="10000"
|
:min="9000"
|
||||||
:max="99999"
|
:max="99999"
|
||||||
placeholder="10000"
|
placeholder="10000"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
v-model="blacklistMinModel" />
|
v-model="blacklistMinModel" />
|
||||||
x
|
~
|
||||||
<InputNumber
|
<InputNumber
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
@ -139,7 +147,36 @@ prf.reload();
|
|||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
v-model="blacklistMaxModel"
|
v-model="blacklistMaxModel"
|
||||||
/></OptionRow> -->
|
/></OptionRow>
|
||||||
|
<OptionRow
|
||||||
|
class="number-input"
|
||||||
|
title="GP"
|
||||||
|
v-if="
|
||||||
|
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-DisableGP')
|
||||||
|
"
|
||||||
|
><InputNumber
|
||||||
|
class="shrink"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
:use-grouping="false"
|
||||||
|
:allow-empty="false"
|
||||||
|
v-model="prf.current!.data.mu3_ini!.gp"
|
||||||
|
/>
|
||||||
|
</OptionRow>
|
||||||
|
<OptionRow
|
||||||
|
title="Unlock Bonus Tracks"
|
||||||
|
tooltip="Disabling this option can help declutter the song list"
|
||||||
|
v-if="
|
||||||
|
prf.current?.data.mods.includes(
|
||||||
|
'7EVENDAYSHOLIDAYS-UnlockAllMusic'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
v-model="prf.current!.data.mu3_ini!.enable_bonus_tracks"
|
||||||
|
/>
|
||||||
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
<KeyboardOptions />
|
<KeyboardOptions />
|
||||||
<StartlinerOptions />
|
<StartlinerOptions />
|
||||||
|
@ -65,6 +65,14 @@ const promptDeleteProfile = async () => {
|
|||||||
accept: deleteProfile,
|
accept: deleteProfile,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dataExists = ref(false);
|
||||||
|
|
||||||
|
path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
|
||||||
|
async (p) => {
|
||||||
|
dataExists.value = await invoke('file_exists', { path: p });
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -124,10 +132,27 @@ const promptDeleteProfile = async () => {
|
|||||||
@click="isEditing = true"
|
@click="isEditing = true"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
rounded
|
||||||
|
icon="pi pi-cog"
|
||||||
|
severity="help"
|
||||||
|
aria-label="open-config-directory"
|
||||||
|
size="small"
|
||||||
|
class="self-center"
|
||||||
|
style="width: 2rem; height: 2rem"
|
||||||
|
@click="
|
||||||
|
path
|
||||||
|
.join(general.configDir, `profile-${p!.game}-${p!.name}`)
|
||||||
|
.then(async (path) => {
|
||||||
|
await invoke('open_file', { path });
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="dataExists"
|
||||||
rounded
|
rounded
|
||||||
icon="pi pi-folder"
|
icon="pi pi-folder"
|
||||||
severity="help"
|
severity="help"
|
||||||
aria-label="open-directory"
|
aria-label="open-data-directory"
|
||||||
size="small"
|
size="small"
|
||||||
class="self-center"
|
class="self-center"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
@ -135,9 +160,7 @@ const promptDeleteProfile = async () => {
|
|||||||
path
|
path
|
||||||
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
|
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
|
||||||
.then(async (path) => {
|
.then(async (path) => {
|
||||||
if (await invoke('file_exists', { path })) {
|
await invoke('open_file', { path });
|
||||||
await invoke('open_file', { path });
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
@ -5,10 +5,12 @@ import ContextMenu from 'primevue/contextmenu';
|
|||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import Onboarding from './Onboarding.vue';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePrfStore } from '../stores';
|
import { useClientStore, usePrfStore } from '../stores';
|
||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
const client = useClientStore();
|
||||||
const confirmDialog = useConfirm();
|
const confirmDialog = useConfirm();
|
||||||
|
|
||||||
type StartStatus = 'ready' | 'preparing' | 'running';
|
type StartStatus = 'ready' | 'preparing' | 'running';
|
||||||
@ -98,6 +100,7 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
label: 'Refresh and start',
|
label: 'Refresh and start',
|
||||||
icon: 'pi pi-sync',
|
icon: 'pi pi-sync',
|
||||||
|
tooltip: 'test',
|
||||||
command: async () => await startline(false, true),
|
command: async () => await startline(false, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -110,6 +113,14 @@ const menuItems = [
|
|||||||
icon: 'pi pi-link',
|
icon: 'pi pi-link',
|
||||||
command: createShortcut,
|
command: createShortcut,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
icon: 'pi pi-question-circle',
|
||||||
|
command: () => {
|
||||||
|
onboardingFirstTime.value = false;
|
||||||
|
onboardingVisible.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const menu = ref();
|
const menu = ref();
|
||||||
|
|
||||||
@ -117,9 +128,38 @@ const showContextMenu = (event: Event) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
menu.value.show(event);
|
menu.value.show(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onboardingVisible = ref(false);
|
||||||
|
const onboardingFirstTime = ref(false);
|
||||||
|
|
||||||
|
const tryStart = () => {
|
||||||
|
const game = prf.current?.meta.game;
|
||||||
|
|
||||||
|
if (game !== undefined) {
|
||||||
|
if (client.onboarded.includes(game)) {
|
||||||
|
startline(false, false);
|
||||||
|
} else {
|
||||||
|
onboardingVisible.value = true;
|
||||||
|
onboardingFirstTime.value = true;
|
||||||
|
client.setOnboarded(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Onboarding
|
||||||
|
:visible="onboardingVisible"
|
||||||
|
:first-time="onboardingFirstTime"
|
||||||
|
:on-finish="
|
||||||
|
() => {
|
||||||
|
onboardingVisible = false;
|
||||||
|
if (onboardingFirstTime === true) {
|
||||||
|
startline(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<ContextMenu ref="menu" :model="menuItems" />
|
<ContextMenu ref="menu" :model="menuItems" />
|
||||||
<Button
|
<Button
|
||||||
v-if="startStatus === 'ready'"
|
v-if="startStatus === 'ready'"
|
||||||
@ -130,7 +170,7 @@ const showContextMenu = (event: Event) => {
|
|||||||
aria-label="start"
|
aria-label="start"
|
||||||
size="small"
|
size="small"
|
||||||
class="m-2.5"
|
class="m-2.5"
|
||||||
@click="startline(false, false)"
|
@click="tryStart"
|
||||||
@contextmenu="showContextMenu"
|
@contextmenu="showContextMenu"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -124,7 +124,7 @@ const prf = usePrfStore();
|
|||||||
<div
|
<div
|
||||||
v-for="idx in Array(16)
|
v-for="idx in Array(16)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => 16 - i)"
|
.map((_, i) => 32 - 2 * i - 1)"
|
||||||
>
|
>
|
||||||
<KeyboardKey
|
<KeyboardKey
|
||||||
button="cell"
|
button="cell"
|
||||||
@ -142,7 +142,7 @@ const prf = usePrfStore();
|
|||||||
<div
|
<div
|
||||||
v-for="idx in Array(16)
|
v-for="idx in Array(16)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => 32 - i)"
|
.map((_, i) => 32 - 2 * i)"
|
||||||
>
|
>
|
||||||
<KeyboardKey
|
<KeyboardKey
|
||||||
button="cell"
|
button="cell"
|
||||||
|
119
src/keyboard.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
const KEY_MAP: { [key: number]: string } = {
|
||||||
|
1: 'M1',
|
||||||
|
2: 'M2',
|
||||||
|
4: 'M3',
|
||||||
|
5: 'M4',
|
||||||
|
6: 'M5',
|
||||||
|
8: 'Backspace',
|
||||||
|
9: 'Tab',
|
||||||
|
12: 'Clear',
|
||||||
|
13: 'Enter',
|
||||||
|
19: 'Pause',
|
||||||
|
20: 'CapsLock',
|
||||||
|
27: 'Escape',
|
||||||
|
32: 'Space',
|
||||||
|
33: 'PageUp',
|
||||||
|
34: 'PageDown',
|
||||||
|
35: 'End',
|
||||||
|
36: 'Home',
|
||||||
|
37: 'ArrowLeft',
|
||||||
|
38: 'ArrowUp',
|
||||||
|
39: 'ArrowRight',
|
||||||
|
40: 'ArrowDown',
|
||||||
|
45: 'Insert',
|
||||||
|
46: 'Delete',
|
||||||
|
48: 'Digit0',
|
||||||
|
49: 'Digit1',
|
||||||
|
50: 'Digit2',
|
||||||
|
51: 'Digit3',
|
||||||
|
52: 'Digit4',
|
||||||
|
53: 'Digit5',
|
||||||
|
54: 'Digit6',
|
||||||
|
55: 'Digit7',
|
||||||
|
56: 'Digit8',
|
||||||
|
57: 'Digit9',
|
||||||
|
65: 'KeyA',
|
||||||
|
66: 'KeyB',
|
||||||
|
67: 'KeyC',
|
||||||
|
68: 'KeyD',
|
||||||
|
69: 'KeyE',
|
||||||
|
70: 'KeyF',
|
||||||
|
71: 'KeyG',
|
||||||
|
72: 'KeyH',
|
||||||
|
73: 'KeyI',
|
||||||
|
74: 'KeyJ',
|
||||||
|
75: 'KeyK',
|
||||||
|
76: 'KeyL',
|
||||||
|
77: 'KeyM',
|
||||||
|
78: 'KeyN',
|
||||||
|
79: 'KeyO',
|
||||||
|
80: 'KeyP',
|
||||||
|
81: 'KeyQ',
|
||||||
|
82: 'KeyR',
|
||||||
|
83: 'KeyS',
|
||||||
|
84: 'KeyT',
|
||||||
|
85: 'KeyU',
|
||||||
|
86: 'KeyV',
|
||||||
|
87: 'KeyW',
|
||||||
|
88: 'KeyX',
|
||||||
|
89: 'KeyY',
|
||||||
|
90: 'KeyZ',
|
||||||
|
91: 'MetaLeft',
|
||||||
|
92: 'MetaRight',
|
||||||
|
93: 'ContextMenu',
|
||||||
|
96: 'Numpad0',
|
||||||
|
97: 'Numpad1',
|
||||||
|
98: 'Numpad2',
|
||||||
|
99: 'Numpad3',
|
||||||
|
100: 'Numpad4',
|
||||||
|
101: 'Numpad5',
|
||||||
|
102: 'Numpad6',
|
||||||
|
103: 'Numpad7',
|
||||||
|
104: 'Numpad8',
|
||||||
|
105: 'Numpad9',
|
||||||
|
106: 'NumpadMultiply',
|
||||||
|
107: 'NumpadAdd',
|
||||||
|
109: 'NumpadSubtract',
|
||||||
|
110: 'NumpadDecimal',
|
||||||
|
111: 'NumpadDivide',
|
||||||
|
112: 'F1',
|
||||||
|
113: 'F2',
|
||||||
|
114: 'F3',
|
||||||
|
115: 'F4',
|
||||||
|
116: 'F5',
|
||||||
|
117: 'F6',
|
||||||
|
118: 'F7',
|
||||||
|
119: 'F8',
|
||||||
|
120: 'F9',
|
||||||
|
121: 'F10',
|
||||||
|
122: 'F11',
|
||||||
|
123: 'F12',
|
||||||
|
144: 'NumLock',
|
||||||
|
145: 'ScrollLock',
|
||||||
|
160: 'ShiftLeft',
|
||||||
|
161: 'ShiftRight',
|
||||||
|
162: 'ControlLeft',
|
||||||
|
163: 'ControlRight',
|
||||||
|
164: 'AltLeft',
|
||||||
|
165: 'AltRight',
|
||||||
|
186: 'Semicolon',
|
||||||
|
187: 'Equal',
|
||||||
|
188: 'Comma',
|
||||||
|
189: 'Minus',
|
||||||
|
190: 'Period',
|
||||||
|
191: 'Slash',
|
||||||
|
192: 'Backquote',
|
||||||
|
219: 'BracketLeft',
|
||||||
|
220: 'Backslash',
|
||||||
|
221: 'BracketRight',
|
||||||
|
222: 'Quote',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromKeycode = (keyCode: number): string | null => {
|
||||||
|
return KEY_MAP[keyCode] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toKeycode = (key: string): number | null => {
|
||||||
|
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
|
||||||
|
return res ? parseInt(res) : null;
|
||||||
|
};
|
@ -357,6 +357,10 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export enum ClientData {
|
||||||
|
Onboarded,
|
||||||
|
}
|
||||||
|
|
||||||
export const useClientStore = defineStore('client', () => {
|
export const useClientStore = defineStore('client', () => {
|
||||||
type ScaleType = 's' | 'm' | 'l' | 'xl';
|
type ScaleType = 's' | 'm' | 'l' | 'xl';
|
||||||
const scaleFactor: Ref<ScaleType> = ref('s');
|
const scaleFactor: Ref<ScaleType> = ref('s');
|
||||||
@ -366,16 +370,21 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
const enableAutoupdates = ref(true);
|
const enableAutoupdates = ref(true);
|
||||||
const verbose = ref(false);
|
const verbose = ref(false);
|
||||||
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
|
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
|
||||||
|
const onboarded: Ref<Game[]> = ref([]);
|
||||||
|
|
||||||
const scaleValue = (value: ScaleType) =>
|
const _scaleValue = (value: ScaleType) =>
|
||||||
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
||||||
|
|
||||||
|
const scaleValue = computed(() => {
|
||||||
|
return _scaleValue(scaleFactor.value);
|
||||||
|
});
|
||||||
|
|
||||||
const setScaleFactor = async (value: ScaleType) => {
|
const setScaleFactor = async (value: ScaleType) => {
|
||||||
scaleFactor.value = value;
|
scaleFactor.value = value;
|
||||||
|
|
||||||
const window = getCurrentWindow();
|
const window = getCurrentWindow();
|
||||||
const w = Math.floor(scaleValue(value) * 900);
|
const w = Math.floor(_scaleValue(value) * 900);
|
||||||
const h = Math.floor(scaleValue(value) * 480);
|
const h = Math.floor(_scaleValue(value) * 600);
|
||||||
|
|
||||||
let size = await window.innerSize();
|
let size = await window.innerSize();
|
||||||
|
|
||||||
@ -420,6 +429,10 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
if (input.theme) {
|
if (input.theme) {
|
||||||
theme.value = input.theme;
|
theme.value = input.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.onboarded) {
|
||||||
|
onboarded.value = input.onboarded;
|
||||||
|
}
|
||||||
await setTheme(theme.value);
|
await setTheme(theme.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error reading client options: ${e}`);
|
console.error(`Error reading client options: ${e}`);
|
||||||
@ -452,6 +465,7 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
h: Math.floor(size.height),
|
h: Math.floor(size.height),
|
||||||
},
|
},
|
||||||
theme: theme.value,
|
theme: theme.value,
|
||||||
|
onboarded: onboarded.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -499,6 +513,11 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
await save();
|
await save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setOnboarded = async (game: Game) => {
|
||||||
|
onboarded.value = [...onboarded.value, game];
|
||||||
|
await save();
|
||||||
|
};
|
||||||
|
|
||||||
getCurrentWindow().onResized(async ({ payload }) => {
|
getCurrentWindow().onResized(async ({ payload }) => {
|
||||||
// For whatever reason this is 0 when minimized
|
// For whatever reason this is 0 when minimized
|
||||||
if (payload.width > 0) {
|
if (payload.width > 0) {
|
||||||
@ -512,8 +531,11 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
enableAutoupdates,
|
enableAutoupdates,
|
||||||
verbose,
|
verbose,
|
||||||
theme,
|
theme,
|
||||||
|
onboarded,
|
||||||
timeout,
|
timeout,
|
||||||
scaleModel,
|
scaleModel,
|
||||||
|
_scaleValue,
|
||||||
|
scaleValue,
|
||||||
load,
|
load,
|
||||||
save,
|
save,
|
||||||
queueSave,
|
queueSave,
|
||||||
@ -521,5 +543,6 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
setAutoupdates,
|
setAutoupdates,
|
||||||
setVerbose,
|
setVerbose,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
setOnboarded,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -107,7 +107,10 @@ export interface BepInExConfig {
|
|||||||
|
|
||||||
export interface Mu3IniConfig {
|
export interface Mu3IniConfig {
|
||||||
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
|
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
|
||||||
// blacklist?: [number, number];
|
sample_rate: number;
|
||||||
|
blacklist?: [number, number];
|
||||||
|
gp: number;
|
||||||
|
enable_bonus_tracks: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OngekiButtons {
|
export interface OngekiButtons {
|
||||||
|
@ -63,3 +63,12 @@ export const messageSplit = (message: any) => {
|
|||||||
export const shouldPreferDark = () => {
|
export const shouldPreferDark = () => {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const prettyPrint = (game: Game) => {
|
||||||
|
switch (game) {
|
||||||
|
case 'ongeki':
|
||||||
|
return 'O.N.G.E.K.I.';
|
||||||
|
case 'chunithm':
|
||||||
|
return 'CHUNITHM';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
|
|||||||
ignored: ['**/rust/**'],
|
ignored: ['**/rust/**'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 1024,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|