Skip to content

Commit ae4df3c

Browse files
authored
Merge branch 'main' of https://github.com/Lauriethefish/ModsBeforeFriday into adb-websocket
2 parents 45e24da + 02af25d commit ae4df3c

37 files changed

Lines changed: 6428 additions & 880 deletions

.github/workflows/build.yml

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ jobs:
2424
url: ${{ steps.deployment.outputs.page_url }}
2525
steps:
2626
- uses: actions/checkout@v4
27+
with:
28+
submodules: 'true'
2729

2830
- name: Agent Hash
2931
id: agent_hash
@@ -102,24 +104,21 @@ jobs:
102104
with:
103105
node-version: 20.x
104106

105-
- name: Modules Cache
106-
if: steps.site_cache.outputs.cache-hit != 'true'
107-
id: modules_cache
108-
uses: actions/cache@v4
107+
- uses: pnpm/action-setup@v2
109108
with:
110-
key: site-modules-${{ hashFiles('mbf-site/yarn.lock')}}
111-
path: |
112-
mbf-site/node_modules
109+
version: 9.5.0
113110

114-
- name: yarn install
115-
if: steps.modules_cache.outputs.cache-hit != 'true' && steps.site_cache.outputs.cache-hit != 'true'
116-
run: yarn --cwd ./mbf-site install
111+
- name: pnpm install
112+
run: pnpm install
117113

118-
- name: yarn build
119-
if: steps.site_cache.outputs.cache-hit != 'true'
120-
run: |
121-
find mbf-site -type f
122-
yarn --cwd ./mbf-site build
114+
- name: pnpm build
115+
run: pnpm recursive run build
116+
117+
- name: pnpm install
118+
run: cd mbf-site && pnpm install
119+
120+
- name: pnpm build
121+
run: cd mbf-site && pnpm build
123122

124123
- name: Setup Pages
125124
uses: actions/configure-pages@v4

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
target/
22
.yarn-integrity
3-
package-id-work/
3+
package-id-work/
4+
5+
multi-user.ps1
6+
multiple-accounts.md
7+
8+
mbf-usb-driver-fix/*
9+
node_modules/**

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "ya-webadb"]
2+
path = ya-webadb
3+
url = https://github.com/Lauriethefish/ya-webadb

mbf-agent/src/downgrading.rs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//! Implementation of diff-chaining to allow any version of Beat Saber to be downgraded to any other, assuming there is a diff
2+
//! from each version to the previous version
3+
//!
4+
//! This module also allows more "direct" diffs to be created for faster downgrading, say several versions at once.
5+
//! A breadth-first-search is used to determine the shortest route (smallest number of diffs) in the database to downgrade one version to another.
6+
7+
use std::{collections::{HashMap, VecDeque}, fs::OpenOptions, io::{BufReader, BufWriter, Read}, path::{Path, PathBuf}};
8+
9+
use log::info;
10+
use mbf_res_man::{models::{Diff, VersionDiffs}, res_cache::ResCache};
11+
use anyhow::{Result, Context, anyhow};
12+
use mbf_res_man::external_res;
13+
use mbf_zip::ZIP_CRC;
14+
use std::ffi::OsStr;
15+
16+
use crate::downloads;
17+
18+
// Gets a map of all accessible Beat Saber versions from the given version
19+
// Key is Beat Saber version, value is the sequence of diffs that need to be applied to reach that version.
20+
// Always returns the sequences with the smallest number of diffs
21+
pub fn get_all_accessible_versions(res_cache: &ResCache, from_version: &str) -> Result<HashMap<String, Vec<VersionDiffs>>> {
22+
let diff_index_edges = get_diff_index_graph(res_cache)
23+
.context("Loading diff index")?;
24+
25+
// Holds the path of diffs used to reach each Beat Saber version
26+
let mut predecessor_map: HashMap<String, Vec<VersionDiffs>> = HashMap::new();
27+
predecessor_map.insert(from_version.to_owned(), Vec::new()); // No diffs needed to reach the current version.
28+
29+
let mut queue = VecDeque::new();
30+
queue.push_back(from_version.to_string());
31+
32+
// Apply breath first search
33+
// TODO: This does tons of copying though it doesn't really matter much since there aren't too many versions
34+
while let Some(curr_ver) = queue.pop_front() {
35+
if let Some(edges) = diff_index_edges.get(&curr_ver) {
36+
for diff in edges {
37+
if !predecessor_map.contains_key(&diff.to_version) {
38+
let mut path = predecessor_map.get(&curr_ver)
39+
.expect("Current version should always have a path").clone();
40+
path.push(diff.clone());
41+
42+
predecessor_map.insert(diff.to_version.clone(), path);
43+
queue.push_back(diff.to_version.clone());
44+
}
45+
}
46+
}
47+
}
48+
49+
predecessor_map.remove(from_version); // Don't want to show this version in the list of downgrade versions
50+
Ok(predecessor_map)
51+
52+
}
53+
54+
// Loads the diff index as a graph, where each entry in the HashMap lists the edges from each node (Beat Saber versions are nodes)
55+
fn get_diff_index_graph(res_cache: &ResCache) -> Result<HashMap<String, Vec<VersionDiffs>>> {
56+
let diff_index = mbf_res_man::external_res::get_diff_index(res_cache)
57+
.context("Fetching downgrading information")?;
58+
59+
let mut diff_index_edges: HashMap<String, Vec<VersionDiffs>> = HashMap::new();
60+
for diff in diff_index {
61+
if let Some(accessible) = diff_index_edges.get_mut(&diff.from_version) {
62+
accessible.push(diff);
63+
} else {
64+
diff_index_edges.insert(diff.from_version.to_owned(), vec![diff]);
65+
}
66+
}
67+
68+
Ok(diff_index_edges)
69+
}
70+
71+
// Downgrades the APK file at `temp_path` and the OBB files at `obb_backup_paths`.
72+
// Determines the sequence of diffs to apply automatically.
73+
// The destination APK is written to `temp_path` and the destination OBBs are written to the same directory as the current OBBs.
74+
// Returns the paths to the downgraded OBB files for restoring once Beat Saber is reinstalled.
75+
pub fn get_and_apply_diff_sequence(from_version: &str, to_version: &str,
76+
temp_path: &Path, temp_apk_path: &Path, obb_backup_paths: Vec<PathBuf>,
77+
res_cache: &ResCache)
78+
-> Result<Vec<PathBuf>> {
79+
info!("Working out diff sequence for {from_version} --> {to_version}");
80+
let diff_sequences = get_all_accessible_versions(res_cache, from_version)
81+
.context("Determining diff sequence")?;
82+
let diffs = diff_sequences.get(to_version)
83+
.ok_or(anyhow!("No diff sequence found for version. Why did the frontend let us select it?!"))?;
84+
85+
apply_diff_sequence(diffs, temp_path, &temp_apk_path, obb_backup_paths)
86+
.context("Downgrading")
87+
}
88+
89+
// Downgrades the APK file at `temp_path` and the OBB files at `obb_backup_paths`.
90+
// The destination APK is written to `temp_path` and the destination OBBs are written to the same directory as the current OBBs.
91+
// Returns the paths to the downgraded OBB files for restoring once Beat Saber is reinstalled.
92+
pub fn apply_diff_sequence(diffs: &[VersionDiffs], temp_path: &Path,
93+
temp_apk_path: &Path,
94+
mut obb_backup_paths: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
95+
info!("DOWNGRADING BEAT SABER: This may take a LONG time");
96+
97+
for (i, diff) in diffs.iter().enumerate() {
98+
info!("Applying diffs set {}/{} ({} --> {})", i + 1, diffs.len(), diff.from_version, diff.to_version);
99+
100+
obb_backup_paths = apply_version_diff(diff, temp_path, &temp_apk_path, obb_backup_paths)
101+
.context("Applying diff")?;
102+
}
103+
104+
Ok(obb_backup_paths)
105+
}
106+
107+
// Downgrades one version of Beat Saber to another, including the APK file and all OBB files
108+
// Passed the path to the temporary APK and paths to each of the existing OBBs for downgrading.
109+
fn apply_version_diff(diffs: &VersionDiffs, temp_path: &Path,
110+
temp_apk_path: &Path, obb_backup_paths: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
111+
// Download the diff files
112+
let diffs_path = temp_path.join("diffs");
113+
std::fs::create_dir_all(&diffs_path).context("Creating diffs directory")?;
114+
info!("Downloading diffs");
115+
download_diffs(&diffs_path, &diffs).context("Downloading diffs")?;
116+
117+
// Copy the APK to temp, downgrading it in the process.
118+
info!("Downgrading APK");
119+
apply_diff(
120+
// If there is already a "downgraded" APK, then we have already applied one diff, so we downgrade THIS APK to the next version.
121+
&temp_apk_path,
122+
&temp_apk_path,
123+
&diffs.apk_diff,
124+
&diffs_path,
125+
)
126+
.context("Applying diff to APK")?;
127+
128+
let mut dest_obb_paths = Vec::new();
129+
for obb_diff in &diffs.obb_diffs {
130+
// Find the OBB matching the filename in the diff
131+
let existing_obb = obb_backup_paths.iter()
132+
.find(|p| p.file_name() == Some(OsStr::new(&obb_diff.file_name)))
133+
.ok_or(anyhow!("No obb file {} found - is the diff index wrong", obb_diff.file_name))?;
134+
135+
// Determine a suitable destination path.
136+
let obbs_folder = existing_obb.parent().unwrap();
137+
let dest_obb = obbs_folder.join(&obb_diff.output_file_name);
138+
139+
apply_diff(&existing_obb, &dest_obb, obb_diff, &diffs_path)
140+
.context("Applying diff to OBB")?;
141+
std::fs::remove_file(existing_obb).context("Deleting old OBB")?; // Save storage space!
142+
dest_obb_paths.push(dest_obb);
143+
}
144+
145+
146+
147+
// Delete diffs when we're done to avoid using too much storage.
148+
std::fs::remove_dir_all(diffs_path)?;
149+
150+
Ok(dest_obb_paths)
151+
}
152+
153+
// Loads the file from from_path into memory, verifies it matches the checksum of the given diff,
154+
// applies the diff and then outputs it to to_path
155+
// `from_path` and `to_path` can be the same if you like. I give you permission.
156+
fn apply_diff(from_path: &Path, to_path: &Path, diff: &Diff, diffs_path: &Path) -> Result<()> {
157+
let diff_content = read_file_vec(diffs_path.join(&diff.diff_name))
158+
.context("Diff could not be opened. Was it downloaded")?;
159+
160+
let patch = qbsdiff::Bspatch::new(&diff_content).context("Diff file was invalid")?;
161+
162+
let file_content = read_file_vec(from_path).context("Reading original file from disk")?;
163+
164+
// Verify the CRC32 hash of the file content.
165+
info!("Verifying installation is unmodified");
166+
let before_crc = ZIP_CRC.checksum(&file_content);
167+
if before_crc != diff.file_crc {
168+
return Err(anyhow!("File CRC {} did not match expected value of {}.
169+
Your installation is corrupted, so MBF can't downgrade it. Reinstall Beat Saber to fix this issue!
170+
Alternatively, if your game is pirated, purchase a legitimate copy of the game.", before_crc, diff.file_crc));
171+
}
172+
173+
// Carry out the downgrade
174+
info!("Applying patch (This step may take a few minutes)");
175+
let mut output_handle = BufWriter::new(
176+
OpenOptions::new()
177+
.truncate(true)
178+
.create(true)
179+
.read(true)
180+
.write(true)
181+
.open(to_path)?,
182+
);
183+
patch.apply(&file_content, &mut output_handle)?;
184+
185+
// TODO: Verify checksum on the result of downgrading?
186+
187+
Ok(())
188+
}
189+
190+
// Downloads the deltas needed for downgrading with the given version_diffs.
191+
// The diffs are saved with names matching `diff_name` in the `Diff` struct.
192+
fn download_diffs(to_path: impl AsRef<Path>, version_diffs: &VersionDiffs) -> Result<()> {
193+
for diff in version_diffs.obb_diffs.iter() {
194+
info!("Downloading diff for OBB {}", diff.file_name);
195+
download_diff_retry(diff, &to_path)?;
196+
}
197+
198+
info!("Downloading diff for APK");
199+
download_diff_retry(&version_diffs.apk_diff, to_path)?;
200+
201+
Ok(())
202+
}
203+
204+
// Attempts to download the given diff DIFF_DOWNLOAD_ATTEMPTS times, returning an error if the final attempt fails.
205+
fn download_diff_retry(diff: &Diff, to_dir: impl AsRef<Path>) -> Result<()> {
206+
let url = external_res::get_diff_url(diff);
207+
let output_path = to_dir.as_ref().join(&diff.diff_name);
208+
209+
downloads::download_file_with_attempts(&crate::get_dl_cfg(), &output_path, &url)
210+
.context("Downloading diff file")?;
211+
Ok(())
212+
}
213+
214+
// Reads the content of the given file path as a Vec
215+
fn read_file_vec(path: impl AsRef<Path>) -> Result<Vec<u8>> {
216+
let handle = std::fs::File::open(path)?;
217+
218+
let mut file_content = Vec::with_capacity(handle.metadata()?.len() as usize);
219+
let mut reader = BufReader::new(handle);
220+
reader.read_to_end(&mut file_content)?;
221+
222+
Ok(file_content)
223+
}

mbf-agent/src/handlers/import.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use std::path::{Path, PathBuf};
33
use crate::{
44
downloads,
55
mod_man::ModManager,
6-
models::response::{self, ImportResultType, Response},
7-
paths,
6+
models::response::{self, ImportResultType, Response}, parameters::PARAMETERS
87
};
98
use anyhow::{anyhow, Context, Result};
109
use log::{debug, info, warn};
@@ -15,8 +14,8 @@ use mbf_zip::ZipFile;
1514
/// # Returns
1615
/// The [Response](requests::Response) to the request (variant `ImportResult`)
1716
pub(super) fn handle_import_mod_url(from_url: String) -> Result<Response> {
18-
std::fs::create_dir_all(paths::MBF_DOWNLOADS)?;
19-
let download_path = Path::new(paths::MBF_DOWNLOADS).join("import_from_url");
17+
std::fs::create_dir_all(&PARAMETERS.mbf_downloads)?;
18+
let download_path = Path::new(&PARAMETERS.mbf_downloads).join("import_from_url");
2019

2120
info!("Downloading {}", from_url);
2221
let filename: Option<String> =
@@ -183,7 +182,7 @@ fn attempt_song_import(from_path: PathBuf) -> Result<ImportResultType> {
183182
let mut zip = ZipFile::open(song_handle).context("Song was invalid ZIP file")?;
184183

185184
if zip.contains_file("info.dat") || zip.contains_file("Info.dat") {
186-
let extract_path = Path::new(paths::CUSTOM_LEVELS)
185+
let extract_path = Path::new(&PARAMETERS.custom_levels)
187186
.join(from_path.file_stem().expect("Must have file stem"));
188187

189188
if extract_path.exists() {

mbf-agent/src/handlers/mod.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use crate::{
77
mod_man::ModManager,
88
models::{
99
request::Request,
10+
request::RequestEnum,
1011
response::{self, Response},
11-
},
12+
}, parameters::PARAMETERS,
1213
};
1314
use anyhow::{anyhow, Context, Result};
1415
use log::info;
@@ -28,11 +29,11 @@ mod utility;
2829
/// # Returns
2930
/// If successful, a [Response] to be sent back to the frontend.
3031
pub fn handle_request(request: Request) -> Result<Response> {
31-
match request {
32-
Request::GetModStatus {
32+
match request.request {
33+
RequestEnum::GetModStatus {
3334
override_core_mod_url,
3435
} => mod_status::handle_get_mod_status(override_core_mod_url),
35-
Request::Patch {
36+
RequestEnum::Patch {
3637
downgrade_to,
3738
remodding,
3839
manifest_mod,
@@ -49,15 +50,15 @@ pub fn handle_request(request: Request) -> Result<Response> {
4950
override_core_mod_url,
5051
vr_splash_path,
5152
),
52-
Request::GetDowngradedManifest { version } => {
53+
RequestEnum::GetDowngradedManifest { version } => {
5354
patching::handle_get_downgraded_manifest(version)
5455
}
55-
Request::RemoveMod { id } => mod_management::handle_remove_mod(id),
56-
Request::SetModsEnabled { statuses } => mod_management::handle_set_mods_enabled(statuses),
57-
Request::Import { from_path } => import::handle_import(from_path, None),
58-
Request::ImportUrl { from_url } => import::handle_import_mod_url(from_url),
59-
Request::FixPlayerData => utility::handle_fix_player_data(),
60-
Request::QuickFix {
56+
RequestEnum::RemoveMod { id } => mod_management::handle_remove_mod(id),
57+
RequestEnum::SetModsEnabled { statuses } => mod_management::handle_set_mods_enabled(statuses),
58+
RequestEnum::Import { from_path } => import::handle_import(from_path, None),
59+
RequestEnum::ImportUrl { from_url } => import::handle_import_mod_url(from_url),
60+
RequestEnum::FixPlayerData => utility::handle_fix_player_data(),
61+
RequestEnum::QuickFix {
6162
override_core_mod_url,
6263
wipe_existing_mods,
6364
} => utility::handle_quick_fix(override_core_mod_url, wipe_existing_mods),
@@ -72,7 +73,7 @@ pub fn handle_request(request: Request) -> Result<Response> {
7273
/// An `Err` variant is returned on failure, for example if Beat Saber isn't installed or the result from `dumpsys` couldn't be parsed.
7374
fn get_app_version_only() -> Result<String> {
7475
let dumpsys_output = Command::new("dumpsys")
75-
.args(["package", crate::APK_ID])
76+
.args(["package", &PARAMETERS.apk_id])
7677
.output()
7778
.context("Invoking dumpsys")?;
7879
let dumpsys_stdout =

0 commit comments

Comments
 (0)