forked from akanyan/STARTLINER
feat: partial support for patches
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use crate::model::config::GlobalConfig;
|
||||
use crate::model::patch::PatchFileVec;
|
||||
use crate::pkg::{Feature, Status};
|
||||
use crate::profiles::Profile;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
@ -17,6 +18,7 @@ pub struct AppData {
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub state: GlobalState,
|
||||
pub patch_set: PatchFileVec,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
@ -37,13 +39,18 @@ impl AppData {
|
||||
None => None
|
||||
};
|
||||
|
||||
let patch_set = PatchFileVec::new(util::config_dir())
|
||||
.map_err(|e| log::error!("unable to load patch set: {e}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
log::debug!("Recent profile: {:?}", profile);
|
||||
|
||||
AppData {
|
||||
profile: profile,
|
||||
pkgs: PackageStore::new(apph.clone()),
|
||||
cfg,
|
||||
state: GlobalState { remain_open: true }
|
||||
state: GlobalState { remain_open: true },
|
||||
patch_set
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ use tokio::fs;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::config::GlobalConfigField;
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::patch::Patch;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::pkg_store::{InstallResult, PackageStore};
|
||||
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
|
||||
@ -442,4 +443,15 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
|
||||
#[tauri::command]
|
||||
pub async fn file_exists(path: String) -> Result<bool, ()> {
|
||||
Ok(std::fs::exists(path).unwrap_or(false))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> Result<Vec<Patch>, String> {
|
||||
log::debug!("invoke: list_patches({})", target);
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.fix();
|
||||
let list = appd.patch_set.find_patches(target).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(list)
|
||||
}
|
@ -7,6 +7,7 @@ mod download_handler;
|
||||
mod appdata;
|
||||
mod modules;
|
||||
mod profiles;
|
||||
mod patcher;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
@ -208,6 +209,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::list_platform_capabilities,
|
||||
cmd::list_directories,
|
||||
cmd::file_exists,
|
||||
|
||||
cmd::list_patches
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
|
@ -3,4 +3,4 @@ pub mod misc;
|
||||
pub mod rainy;
|
||||
pub mod profile;
|
||||
pub mod config;
|
||||
pub mod segatools_base;
|
||||
pub mod patch;
|
155
rust/src/model/patch.rs
Normal file
155
rust/src/model/patch.rs
Normal file
@ -0,0 +1,155 @@
|
||||
use std::collections::BTreeMap;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use serde::ser::{Serializer, SerializeStruct};
|
||||
use serde_json::Value;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub enum PatchSelectionData {
|
||||
Enabled,
|
||||
Number(i8),
|
||||
Hex(Vec<u8>)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PatchFileVec(pub Vec<PatchFile>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchFile(pub Vec<PatchList>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchList {
|
||||
pub filename: String,
|
||||
pub version: String,
|
||||
pub sha256: String,
|
||||
pub patches: Vec<Patch>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Patch {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub tooltip: Option<String>,
|
||||
pub data: PatchData
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum PatchData {
|
||||
Normal(NormalPatch),
|
||||
Number(NumberPatch),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NormalPatch {
|
||||
pub patches: Vec<NormalPatchField>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NormalPatchField {
|
||||
pub offset: u64,
|
||||
pub off: Vec<u8>,
|
||||
pub on: Vec<u8>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NumberPatch {
|
||||
pub offset: u64,
|
||||
pub size: i64,
|
||||
pub min: i32,
|
||||
pub max: i32
|
||||
}
|
||||
|
||||
impl Serialize for Patch {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let mut state = serializer.serialize_struct("Patch", 7)?;
|
||||
state.serialize_field("id", &self.id)?;
|
||||
state.serialize_field("name", &self.name)?;
|
||||
state.serialize_field("tooltip", &self.tooltip)?;
|
||||
match &self.data {
|
||||
PatchData::Normal(patch) => {
|
||||
state.serialize_field("patches", &patch.patches)?;
|
||||
}
|
||||
PatchData::Number(patch) => {
|
||||
state.serialize_field("type", "number")?;
|
||||
state.serialize_field("offset", &patch.offset)?;
|
||||
state.serialize_field("size", &patch.size)?;
|
||||
state.serialize_field("min", &patch.min)?;
|
||||
state.serialize_field("max", &patch.max)?;
|
||||
}
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Patch {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let value = Value::deserialize(d)?;
|
||||
|
||||
let data = Ok(match value.get("type").and_then(Value::as_str) {
|
||||
Some("number") => PatchData::Number(NumberPatch {
|
||||
offset: value.get("offset")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| de::Error::missing_field("offset"))?,
|
||||
size: value.get("size")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("size"))?,
|
||||
min: i32::try_from(value.get("min")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("min"))?
|
||||
).map_err(|_| de::Error::missing_field("min"))?,
|
||||
max: i32::try_from(value.get("min")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("min"))?
|
||||
).map_err(|_| de::Error::missing_field("min"))?
|
||||
}),
|
||||
None => {
|
||||
let mut patches = vec![];
|
||||
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
|
||||
let mut off_list: Vec<u8> = Vec::new();
|
||||
let mut on_list: Vec<u8> = Vec::new();
|
||||
for off in patch.get("off").and_then(Value::as_array).unwrap() {
|
||||
off_list.push(u8::try_from(
|
||||
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
|
||||
).map_err(|_| de::Error::missing_field("off"))?);
|
||||
}
|
||||
for on in patch.get("on").and_then(Value::as_array).unwrap() {
|
||||
on_list.push(u8::try_from(
|
||||
on.as_u64().ok_or_else(|| de::Error::missing_field("on"))?
|
||||
).map_err(|_| de::Error::missing_field("on"))?);
|
||||
}
|
||||
patches.push(NormalPatchField {
|
||||
offset: patch.get("offset")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| de::Error::missing_field("offset"))?,
|
||||
off: off_list,
|
||||
on: on_list
|
||||
})
|
||||
}
|
||||
PatchData::Normal(NormalPatch {
|
||||
patches
|
||||
})
|
||||
},
|
||||
Some(&_) => return Err(de::Error::custom("unsupported type"))
|
||||
});
|
||||
|
||||
Ok(Patch {
|
||||
id: value.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| de::Error::missing_field("id"))?
|
||||
.to_owned(),
|
||||
name: value.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| de::Error::missing_field("name"))?
|
||||
.to_owned(),
|
||||
tooltip: value.get("tooltip")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|s| Some(s.to_owned())),
|
||||
data: data?
|
||||
})
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
use super::misc::Game;
|
||||
|
||||
pub fn segatools_base(game: Game) -> String {
|
||||
match game {
|
||||
Game::Ongeki =>
|
||||
"[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
enable=1
|
||||
|
||||
[system]
|
||||
; Enable ALLS system settings.
|
||||
enable=1
|
||||
|
||||
; Enable freeplay mode. This will disable the coin slot and set the game to
|
||||
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
|
||||
; allow you to start a game in freeplay mode.
|
||||
freeplay=0
|
||||
|
||||
; LAN Install: Set this to 1 on all machines.
|
||||
dipsw1=1
|
||||
|
||||
[gfx]
|
||||
; Enables the graphics hook.
|
||||
enable=1
|
||||
|
||||
[led15093]
|
||||
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
|
||||
; RGBs and the rear LED panel (billboard) on the cabinet.
|
||||
enable=1
|
||||
|
||||
[led]
|
||||
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
|
||||
cabLedOutputPipe=1
|
||||
; Output billboard LED strip data to serial
|
||||
cabLedOutputSerial=0
|
||||
|
||||
; Output slider LED data to the named pipe
|
||||
controllerLedOutputPipe=1
|
||||
; Output slider LED data to the serial port
|
||||
controllerLedOutputSerial=0".to_owned(),
|
||||
Game::Chunithm => "
|
||||
[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
enable=1
|
||||
|
||||
[system]
|
||||
; Enable ALLS system settings.
|
||||
enable=1
|
||||
|
||||
; Enable freeplay mode. This will disable the coin slot and set the game to
|
||||
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
|
||||
; allow you to start a game in freeplay mode.
|
||||
freeplay=0
|
||||
|
||||
; LAN Install: If multiple machines are present on the same LAN then set
|
||||
; this to 1 on exactly one machine and set this to 0 on all others.
|
||||
dipsw1=1
|
||||
; Monitor type: 0 = 120FPS, 1 = 60FPS
|
||||
dipsw2=1
|
||||
; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch
|
||||
; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well.
|
||||
dipsw3=1
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; Misc. hooks settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[gfx]
|
||||
; Enables the graphics hook.
|
||||
enable=1
|
||||
; Force the game to run windowed.
|
||||
windowed=1
|
||||
; Add a frame to the game window if running windowed.
|
||||
framed=0
|
||||
; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen)
|
||||
monitor=0
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; LED settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[led15093]
|
||||
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
|
||||
; RGBs and the rear LED panel (billboard) on the cabinet.
|
||||
enable=1
|
||||
|
||||
[led]
|
||||
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\"
|
||||
cabLedOutputPipe=1
|
||||
; Output billboard LED strip data to serial
|
||||
cabLedOutputSerial=0
|
||||
|
||||
; Output slider LED data to the named pipe
|
||||
controllerLedOutputPipe=1
|
||||
; Output slider LED data to the serial port
|
||||
controllerLedOutputSerial=0
|
||||
; Use the OpeNITHM protocol for serial LED output
|
||||
controllerLedOutputOpeNITHM=0
|
||||
|
||||
; Serial port to send data to if using serial output. Default is COM5.
|
||||
;serialPort=COM5
|
||||
; Baud rate for serial data (set to 115200 if using OpeNITHM)
|
||||
;serialBaud=921600
|
||||
|
||||
; Data output a sequence of bytes, with JVS-like framing.
|
||||
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
|
||||
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
|
||||
; it and use the next sent byte plus one instead.
|
||||
;
|
||||
; After the sync is one byte for the board number that was updated, followed by
|
||||
; the red, green and blue values for each LED.
|
||||
;
|
||||
; Board 0 has 53 LEDs:
|
||||
; [0]-[49]: snakes through left half of billboard (first column starts at top)
|
||||
; [50]-[52]: left side partition LEDs
|
||||
;
|
||||
; Board 1 has 63 LEDs:
|
||||
; [0]-[59]: right half of billboard (first column starts at bottom)
|
||||
; [60]-[62]: right side partition LEDs
|
||||
;
|
||||
; Board 2 is the slider and has 31 LEDs:
|
||||
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
|
||||
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; Custom IO settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[chuniio]
|
||||
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
|
||||
; (will use chu2to3 engine internally)
|
||||
;path=
|
||||
|
||||
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
|
||||
; x86 chuniio to path32, x64 to path64. Both are necessary.
|
||||
;path32=
|
||||
;path64=
|
||||
".to_owned()
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ini::Ini;
|
||||
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||
use crate::{model::{misc::Game, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||
use crate::pkg_store::PackageStore;
|
||||
|
||||
impl Segatools {
|
||||
@ -66,8 +66,12 @@ impl Segatools {
|
||||
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||
|
||||
if !ini_path.exists() {
|
||||
tokio::fs::write(&ini_path, segatools_base(game)).await
|
||||
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||
match game {
|
||||
Game::Ongeki => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-ongeki.ini"))
|
||||
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?,
|
||||
Game::Chunithm => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-chunithm.ini"))
|
||||
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?
|
||||
}
|
||||
}
|
||||
if !pfx_dir.exists() {
|
||||
tokio::fs::create_dir(&pfx_dir).await
|
||||
|
45
rust/src/patcher.rs
Normal file
45
rust/src/patcher.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use sha256::try_digest;
|
||||
|
||||
use crate::model::patch::{Patch, PatchFile, PatchFileVec};
|
||||
|
||||
impl PatchFileVec {
|
||||
pub fn new(config_path: impl AsRef<Path>) -> Result<PatchFileVec> {
|
||||
let path = config_path.as_ref().join("patches");
|
||||
if !path.exists() {
|
||||
std::fs::create_dir(&path)?;
|
||||
}
|
||||
std::fs::write(path.join("builtin-chunithm.json5"), include_bytes!("../static/standard-chunithm.json5"))?;
|
||||
let mut res = Vec::new();
|
||||
for f in std::fs::read_dir(path)? {
|
||||
let f = f?;
|
||||
let f = f.path();
|
||||
res.push(
|
||||
serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?)?
|
||||
);
|
||||
}
|
||||
Ok(PatchFileVec(res))
|
||||
}
|
||||
|
||||
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
|
||||
let checksum = try_digest(target.as_ref())?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for pfile in &self.0 {
|
||||
for plist in &pfile.0 {
|
||||
log::debug!("checking {}", plist.sha256);
|
||||
if plist.sha256 == checksum {
|
||||
let mut cloned = plist.clone().patches;
|
||||
res.append(&mut cloned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res.len() == 0 {
|
||||
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ pub enum Status {
|
||||
}
|
||||
|
||||
#[bitflags]
|
||||
#[repr(u8)]
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Feature {
|
||||
Mod,
|
||||
@ -48,6 +48,10 @@ pub enum Feature {
|
||||
Mu3Hook,
|
||||
Mu3IO,
|
||||
ChusanHook,
|
||||
ChuniIO,
|
||||
Mempatcher,
|
||||
GameDLL,
|
||||
AmdDLL
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@ -224,6 +228,12 @@ impl Package {
|
||||
flags |= Feature::Aime;
|
||||
} else if module == "mu3io" {
|
||||
flags |= Feature::Mu3IO;
|
||||
} else if module == "chuniio" {
|
||||
flags |= Feature::ChuniIO;
|
||||
} else if module == "mempatcher" {
|
||||
flags |= Feature::Mempatcher;
|
||||
} else if module == "game-dll" {
|
||||
flags |= Feature::GameDLL;
|
||||
}
|
||||
}
|
||||
return Status::OK(flags);
|
||||
|
@ -1,11 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, patch::PatchSelection, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
use crate::model::profile::BepInEx;
|
||||
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base};
|
||||
use crate::model::profile::{Display, DisplayMode, Network, Segatools};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
@ -57,7 +57,10 @@ pub struct ProfileData {
|
||||
pub mu3_ini: Option<Mu3Ini>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keyboard: Option<Keyboard>
|
||||
pub keyboard: Option<Keyboard>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub patches: Option<PatchSelection>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
@ -83,13 +86,18 @@ impl Profile {
|
||||
} else {
|
||||
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
|
||||
},
|
||||
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
|
||||
},
|
||||
meta: meta.clone()
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::create_dir_all(p.config_dir())?;
|
||||
std::fs::create_dir_all(p.data_dir())?;
|
||||
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
|
||||
|
||||
match meta.game {
|
||||
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"))?,
|
||||
};
|
||||
|
||||
Ok(p)
|
||||
}
|
||||
@ -101,11 +109,17 @@ impl Profile {
|
||||
|
||||
log::debug!("{:?}", data);
|
||||
|
||||
// Backwards compat
|
||||
if game == Game::Ongeki && data.keyboard.is_none() {
|
||||
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
|
||||
}
|
||||
if game == Game::Chunithm && data.keyboard.is_none() {
|
||||
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
|
||||
if game == Game::Chunithm {
|
||||
if data.keyboard.is_none() {
|
||||
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
|
||||
}
|
||||
if data.patches.is_none() {
|
||||
data.patches = Some(PatchSelection(BTreeMap::new()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Profile {
|
||||
@ -183,6 +197,10 @@ impl Profile {
|
||||
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
|
||||
self.data.keyboard = source.keyboard;
|
||||
}
|
||||
|
||||
if self.data.patches.is_some() && source.patches.is_some() {
|
||||
self.data.patches = source.patches;
|
||||
}
|
||||
}
|
||||
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> {
|
||||
let info = match &self.data.display {
|
||||
|
Reference in New Issue
Block a user