diff --git a/Cargo.toml b/Cargo.toml index 8d05cd7..017ae2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = ["bepinex_installer/", "bepinex_helpers/", "bepinex_sources/"] [workspace.dependencies] semver = "1.0.14" +anyhow = "1.0.65" lazy_static = "1" [profile.release] diff --git a/bepinex_helpers/src/game.rs b/bepinex_helpers/src/game.rs index 8976cde..fbf9de2 100644 --- a/bepinex_helpers/src/game.rs +++ b/bepinex_helpers/src/game.rs @@ -7,6 +7,33 @@ use std::{ }; use steamlocate::SteamDir; +#[macro_export] +macro_rules! game_type { + ($ty:expr) => { + $ty.as_ref() + .unwrap() + .to_string() + .split('.') + .collect::>() + .join("") + }; +} + +#[derive(Debug)] +pub enum GameArch { + X64, + X86, +} + +impl Display for GameArch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GameArch::X64 => write!(f, "x64"), + GameArch::X86 => write!(f, "x86"), + } + } +} + #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum GameType { UnityMono, @@ -16,8 +43,8 @@ pub enum GameType { impl Display for GameType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - GameType::UnityMono => write!(f, "UnityMono"), - GameType::UnityIL2CPP => write!(f, "UnityIL2CPP"), + GameType::UnityMono => write!(f, "Unity.Mono"), + GameType::UnityIL2CPP => write!(f, "Unity.IL2CPP"), } } } @@ -32,12 +59,12 @@ pub struct Game { } impl Game { - pub fn set_bix(&mut self, bix: Option) { - self.bepinex_version = bix; + pub fn set_bie(&mut self, bie: Option) { + self.bepinex_version = bie; } - pub fn set_arch(&mut self, arch: String) { - self.arch = arch; + pub fn set_arch(&mut self, arch: GameArch) { + self.arch = arch.to_string(); } pub fn set_ty(&mut self, ty: Option) { @@ -59,6 +86,22 @@ impl Game { } } + pub fn get_game_arch(&self) -> GameArch { + let path = &self.path.join(format!("{}.exe", &self.name)); + fs::read(path) + .map(|bytes| { + let start = + i32::from_le_bytes(bytes[60..64].try_into().unwrap_or_default()) as usize; + let machine_type = + u16::from_le_bytes(bytes[start + 4..start + 6].try_into().unwrap_or_default()); + match machine_type { + 34404 => GameArch::X64, + _ => GameArch::X86, + } + }) + .unwrap_or_else(|_| GameArch::X64) + } + pub fn get_game_type(&self) -> Option { let mono = "Managed"; let il2cpp = "il2cpp_data"; @@ -96,20 +139,6 @@ impl Default for Game { } } -impl Game { - pub fn to_query(&self, target: &Version) -> String { - match target.major { - 6 => format!( - "BepInEx_{}_{}_{}.zip", - self.ty.as_ref().unwrap(), - self.arch, - target - ), - _ => format!("BepInEx_{}_{}.0.zip", self.arch, target), - } - } -} - impl Display for Game { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) @@ -135,10 +164,12 @@ pub fn get_unity_games() -> Result, Box> { ty: None, }; - let bix_ver = game.get_installed_bepinex_version(); + let bie_ver = game.get_installed_bepinex_version(); let game_type = game.get_game_type(); - game.set_bix(bix_ver); + let game_arch = game.get_game_arch(); + game.set_bie(bie_ver); game.set_ty(game_type); + game.set_arch(game_arch); Some(game) } @@ -175,6 +206,6 @@ pub fn get_dll_version(path: PathBuf) -> Result> return Ok(Version::parse(&ver).unwrap()); } - // TODO: Do some proper handling of invalid semver that bix has in older versions 💀 + // TODO: Do some proper handling of invalid semver that bie has in older versions 💀 Ok(Version::parse(version).unwrap()) } diff --git a/bepinex_installer/Cargo.toml b/bepinex_installer/Cargo.toml index 9537f65..2c080eb 100644 --- a/bepinex_installer/Cargo.toml +++ b/bepinex_installer/Cargo.toml @@ -3,11 +3,6 @@ name = "bepinex-installer" version = "0.2.0" edition = "2021" -# [profile.release] -# strip = true -# lto = true -# panic = "abort" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -15,7 +10,9 @@ bepinex_helpers = { path = "../bepinex_helpers" } bepinex_sources = { path = "../bepinex_sources" } eframe = "0.19.0" egui_extras = "0.19.0" -semver = { workspace = true } -lazy_static = { workspace = true } -reqwest = { version = "0.11.12", features = ["json", "blocking"] } +semver.workspace = true +lazy_static.workspace = true egui-toast = "0.4.0" +poll-promise = "0.1.0" +anyhow.workspace = true +parking_lot = "0.12.1" diff --git a/bepinex_installer/README.md b/bepinex_installer/README.md index 8a7a97e..04d60fd 100644 --- a/bepinex_installer/README.md +++ b/bepinex_installer/README.md @@ -4,6 +4,6 @@ | Name | Description | Status | | ------------------------ | ----------------------- | :----: | | Stable Releases | Install stable releases | ✔ | -| BE Releases | Install BE releases | ❌ | +| BE Releases | Install BE releases | ✔ | | Better UI | Make UI look pretty | 👷‍♀️ | | Support other game types | Support for .NET games | ❌ | diff --git a/bepinex_installer/src/installer.rs b/bepinex_installer/src/installer.rs index e516db8..29c74f5 100644 --- a/bepinex_installer/src/installer.rs +++ b/bepinex_installer/src/installer.rs @@ -1,12 +1,12 @@ use std::time::Duration; use bepinex_helpers::game::{Game, GameType}; -use bepinex_sources::bepinex::{AssetDownloader, BepInEx, BepInExRelease, ReleaseFlavor}; +use bepinex_sources::{ + bepinex::{AssetDownloader, BepInEx, BepInExRelease, ReleaseFlavor}, + version::VersionExt, +}; use eframe::{ - egui::{ - Button, CentralPanel, ComboBox, Direction, FontFamily::Proportional, FontId, RichText, - TextStyle, Ui, - }, + egui::{CentralPanel, ComboBox, Direction, FontFamily::Proportional, FontId, TextStyle, Ui}, App, }; use egui_extras::{Size, StripBuilder}; @@ -14,36 +14,27 @@ use egui_toast::{ToastOptions, Toasts}; use crate::MIN_IL2CPP_STABLE_VERSION; -#[derive(Default, Debug)] +#[derive(Default)] pub struct Installer { - pub settings: bool, - pub advanced_mode: bool, - pub advanced_settings: Option, + pub release_flavor: ReleaseFlavor, pub bepinex: BepInEx, - pub selected_bix: Option, + pub selected_bie: Option, pub games: Vec, pub selected_game: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AdvancedSettings { - picker: bool, - bleeding_edge: bool, + pub dl_promise: Option>>, + pub shown_toast: bool, } impl Installer { fn show_games_select(self: &mut Installer, ui: &mut Ui) { - let size = 45.; - ui.style_mut().text_styles = [(TextStyle::Button, FontId::new(size, Proportional))].into(); - ComboBox::from_id_source("game_selector") .width(ui.available_width() - 8.0) - .selected_text(RichText::new( + .selected_text( self.selected_game .as_ref() .map(|e| format!("{}", e)) .unwrap_or_else(|| "Select a game".to_owned()), - )) + ) .show_ui(ui, |ui| { for game in self.games.iter() { ui.selectable_value(&mut self.selected_game, Some(game.to_owned()), &game.name); @@ -51,37 +42,74 @@ impl Installer { }); } - fn show_bix_select(self: &mut Installer, ui: &mut Ui) { - let size = 45.; - ui.style_mut().text_styles = [(TextStyle::Button, FontId::new(size, Proportional))].into(); - - ComboBox::from_id_source("bix_selector") + fn show_bie_select(self: &mut Installer, ui: &mut Ui) { + ComboBox::from_id_source("bie_selector") .width(ui.available_width() - 8.0) .selected_text( - self.selected_bix + &self + .selected_bie .as_ref() - .map(|e| format!("{}", e)) - .unwrap_or_else(|| "Select BepInEx version".to_owned()), + .map(|b| b.to_string()) + .unwrap_or_else(|| "None".to_string()), ) .show_ui(ui, |ui| { - for bix_ver in self.bepinex.releases.iter().filter(|r| { - // .eq() because cargo fmt would move '== self' on a new line which makes it look ugly 🙄 - r.flavor.eq(self - .advanced_settings - .map(|s| match s.bleeding_edge { - true => &ReleaseFlavor::BleedingEdge, - false => &ReleaseFlavor::Stable, - }) - .unwrap_or(&ReleaseFlavor::Stable)) - }) { + for bie_ver in self + .bepinex + .releases + .iter() + .filter(|r| r.flavor == self.release_flavor) + { ui.selectable_value( - &mut self.selected_bix, - Some(bix_ver.to_owned()), - &bix_ver.version.to_string(), + &mut self.selected_bie, + Some(bie_ver.to_owned()), + &bie_ver.version.to_string(), ); } }); } + + fn release_flavor_select(self: &mut Installer, ui: &mut Ui) { + ui.radio_value( + &mut self.release_flavor, + ReleaseFlavor::Stable, + ReleaseFlavor::Stable.to_string(), + ); + ui.radio_value( + &mut self.release_flavor, + ReleaseFlavor::BleedingEdge, + ReleaseFlavor::BleedingEdge.to_string(), + ); + } + + fn install_bie(self: &mut Installer, toasts: &mut Toasts, options: ToastOptions) { + if let (Some(selected_game), Some(selected_bie)) = (&self.selected_game, &self.selected_bie) + { + let supported_ver = selected_game.ty == Some(GameType::UnityIL2CPP) + && selected_bie.version >= *MIN_IL2CPP_STABLE_VERSION; + + let supported = supported_ver || (selected_game.ty != Some(GameType::UnityIL2CPP)); + if !supported { + toasts.error( + format!( + "Minimal BepInEx for this game is {}", + *MIN_IL2CPP_STABLE_VERSION + ), + options, + ); + return; + } + + let query = selected_bie.to_query(selected_game); + if let Some(asset) = selected_bie.select_asset(query) { + let game = selected_game.clone(); + self.dl_promise = Some(poll_promise::Promise::spawn_thread("dl", move || { + asset.download(&game) + })); + } else { + toasts.error("Failed to find asset", options); + } + } + } } impl App for Installer { @@ -92,32 +120,51 @@ impl App for Installer { .align_to_end(false); CentralPanel::default().show(ctx, |ui| { StripBuilder::new(ui) - .size(Size::exact(50.0)) - .size(Size::exact(50.0)) + .size(Size::exact(30.0)) + .size(Size::exact(30.0)) + .size(Size::exact(30.0)) .size(Size::remainder()) .vertical(|mut strip| { - strip.cell(|ui| { - self.show_games_select(ui); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + strip.cell(|ui| { + ui.horizontal_centered(|ui| ui.label("Unity game")); + }); + strip.cell(|ui| { + ui.horizontal_centered(|ui| self.show_games_select(ui)); + }); + }); }); - strip.cell(|ui| { - self.show_bix_select(ui); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + strip.cell(|ui| { + ui.horizontal_centered(|ui| ui.label("Version")); + }); + strip.cell(|ui| { + ui.horizontal_centered(|ui| self.show_bie_select(ui)); + }); + }); + }); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + strip.cell(|ui| { + ui.horizontal_centered(|ui| ui.label("Release type")); + }); + strip.cell(|ui| { + ui.horizontal_centered(|ui| { + self.release_flavor_select(ui); + }); + }); + }); }); - strip.strip(|builder| { builder .size(Size::remainder()) .size(Size::exact(40.0)) .vertical(|mut strip| { - if let (Some(selected_game), Some(selected_bix)) = - (&self.selected_game, &self.selected_bix) + if let (Some(selected_game), Some(_selected_bie)) = + (&self.selected_game, &self.selected_bie) { - let supported_ver = selected_game.ty - == Some(GameType::UnityIL2CPP) - && selected_bix.version >= *MIN_IL2CPP_STABLE_VERSION; - - let supported = supported_ver - || (selected_game.ty != Some(GameType::UnityIL2CPP)); - strip.cell(|ui| { ui.style_mut().text_styles = [ (TextStyle::Body, FontId::new(20.0, Proportional)), @@ -136,7 +183,7 @@ impl App for Installer { ui.horizontal(|ui| { ui.label("Installed BepInEx:"); match &selected_game.bepinex_version { - Some(bix) => ui.monospace(bix.to_string()), + Some(ver) => ui.monospace(ver.display()), None => ui.monospace("None"), } }); @@ -144,35 +191,27 @@ impl App for Installer { }); strip.cell(|ui| { ui.centered_and_justified(|ui| { - let install_btn = Button::new("Install").small(); - if ui.add(install_btn).clicked() { - let options = ToastOptions { - show_icon: true, - ..ToastOptions::with_duration( - Duration::from_secs(5), - ) - }; - - if !supported { - toasts.error(format!("Minimal BepInEx for this game is {}", *MIN_IL2CPP_STABLE_VERSION), options); - return; - } - - let query = - selected_game.to_query(&selected_bix.version); - let res = selected_bix - .assets - .download(query, selected_game); - match res { - Ok(_) => { - toasts.success( - "Start the game so you can install mods.", - options, - ); - } - Err(e) => { + let options = ToastOptions { + show_icon: true, + ..ToastOptions::with_duration(Duration::from_secs( + 2, + )) + }; + if ui.button("Install").clicked() { + self.shown_toast = false; + self.install_bie(&mut toasts, options); + } + if let Some(dl_promise) = &self.dl_promise { + if let Some(r) = dl_promise.ready() { + if let Err(e) = r { toasts.error(e.to_string(), options); + } else { + toasts.success("Installed.", options); } + self.dl_promise = None; + } else if !self.shown_toast { + toasts.info("Downloading...", options); + self.shown_toast = true; } } }); @@ -184,5 +223,6 @@ impl App for Installer { }); toasts.show(ctx); + ctx.request_repaint(); } } diff --git a/bepinex_installer/src/main.rs b/bepinex_installer/src/main.rs index 21cf397..419359a 100644 --- a/bepinex_installer/src/main.rs +++ b/bepinex_installer/src/main.rs @@ -5,6 +5,7 @@ pub mod installer; use bepinex_helpers::game::get_unity_games; use bepinex_sources::{ bepinex::{BepInEx, BepInExRelease}, + builds::BuildsApi, github::GitHubApi, }; use eframe::{egui, run_native, NativeOptions}; @@ -16,17 +17,22 @@ use crate::installer::Installer; lazy_static! { pub static ref MIN_SUPPORTED_STABLE_VERSION: Version = Version::parse("5.4.11").unwrap(); pub static ref MIN_IL2CPP_STABLE_VERSION: Version = Version::parse("6.0.0-pre.1").unwrap(); - pub static ref MIN_SUPPORTED_BE_VERSION: Version = Version::parse("6.0.0-be.510").unwrap(); } fn main() { + // TODO: populate Installer while the app running instead. let mut gh = GitHubApi::new("BepInEx", "BepInEx"); gh.set_pre_releases(true); gh.set_min_tag(Some(MIN_SUPPORTED_STABLE_VERSION.clone())); let stable_releases = gh.get_all().unwrap_or_default(); - let releases: Vec = stable_releases.into_iter().map(|r| r.into()).collect(); + let be = BuildsApi::new("https://builds.bepinex.dev"); + let be_builds = be.get_builds().unwrap_or_default(); + + let mut releases: Vec = Vec::new(); + releases.extend(stable_releases.into_iter().map(|r| r.into())); + releases.extend(be_builds.into_iter().map(|r| r.into())); let games = get_unity_games(); if games.is_err() { @@ -35,7 +41,7 @@ fn main() { let mut games = games.unwrap(); games.sort(); - let min_size = Some(egui::vec2(300.0, 450.0)); + let min_size = Some(egui::vec2(400.0, 450.0)); let options = NativeOptions { follow_system_theme: true, transparent: false, @@ -44,8 +50,11 @@ fn main() { ..NativeOptions::default() }; + let bepinex = BepInEx { releases }; + let installer = Installer { - bepinex: BepInEx { releases }, + bepinex: bepinex.clone(), + selected_bie: bepinex.latest(), games, ..Installer::default() }; diff --git a/bepinex_sources/Cargo.toml b/bepinex_sources/Cargo.toml index 0ef79a9..f5765f6 100644 --- a/bepinex_sources/Cargo.toml +++ b/bepinex_sources/Cargo.toml @@ -6,9 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bepinex_helpers = { path = "../bepinex_helpers" } -anyhow = "1.0.65" +lazy_static.workspace = true +bepinex_helpers = { version ="0.1.0", path = "../bepinex_helpers" } +anyhow.workspace = true reqwest = { version = "0.11.12", features = ["blocking", "json"] } semver = { workspace = true, features = ["serde"] } serde = { version = "1.0.145", features = ["derive"] } zip = "0.6.3" +scraper = "0.13.0" +regex = { version = "1.6.0", features = ["pattern"] } \ No newline at end of file diff --git a/bepinex_sources/README.md b/bepinex_sources/README.md index d83a4c6..0355483 100644 --- a/bepinex_sources/README.md +++ b/bepinex_sources/README.md @@ -5,4 +5,4 @@ Crate for working with different BepInEx sources such as [Bleeding edge builds]( | Source | Access all releases | Download specific release | Filter out releases | SourceRelease type into general type | | ------------------ | :-----------------: | ------------------------- | ------------------- | ------------------------------------ | | github.com | ✔ | ✔ | ✔ | ✔ | -| builds.bepinex.dev | ❌ | ❌ | ❌ | ❌ | \ No newline at end of file +| builds.bepinex.dev | ✔ | ✔ | ✔ | ✔ | \ No newline at end of file diff --git a/bepinex_sources/examples/basic.rs b/bepinex_sources/examples/basic.rs index 0b7d315..737398b 100644 --- a/bepinex_sources/examples/basic.rs +++ b/bepinex_sources/examples/basic.rs @@ -8,8 +8,8 @@ fn main() -> anyhow::Result<()> { gh.set_min_tag(Some(min_ver)); let releases = gh.get_all()?; - let bix_releases: Vec = releases.into_iter().map(|r| r.into()).collect(); + let bie_releases: Vec = releases.into_iter().map(|r| r.into()).collect(); - println!("{bix_releases:#?}"); + println!("{bie_releases:#?}"); Ok(()) } diff --git a/bepinex_sources/examples/be.rs b/bepinex_sources/examples/be.rs new file mode 100644 index 0000000..6dc2332 --- /dev/null +++ b/bepinex_sources/examples/be.rs @@ -0,0 +1,11 @@ +use bepinex_sources::{bepinex::BepInExRelease, builds::BuildsApi}; + +fn main() -> anyhow::Result<()> { + let mut builds = BuildsApi::new("https://builds.bepinex.dev"); + builds.set_min_build_id(Some(657)); + let b = builds.get_builds()?; + let bie_releases: Vec = b.into_iter().map(|r| r.into()).collect(); + + println!("{bie_releases:#?}"); + Ok(()) +} diff --git a/bepinex_sources/src/bepinex.rs b/bepinex_sources/src/bepinex.rs index 80e12a1..a5b5648 100644 --- a/bepinex_sources/src/bepinex.rs +++ b/bepinex_sources/src/bepinex.rs @@ -1,51 +1,147 @@ -use std::{collections::HashMap, fmt::Display, io::Cursor}; +use std::{fmt::Display, io::Cursor}; -use bepinex_helpers::game::Game; +use bepinex_helpers::{game::Game, game_type}; use semver::Version; use zip::ZipArchive; -use crate::models::github::releases::GitHubRelease; +use crate::{ + models::{bleeding_edge::builds::BuildsRelease, github::releases::GitHubRelease}, + version::VersionExt, +}; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone)] pub struct BepInEx { pub releases: Vec, } +impl BepInEx { + pub fn latest(&self) -> Option { + self.releases + .iter() + .filter(|r| r.flavor == ReleaseFlavor::Stable) + .map(|r| r.to_owned()) + .collect::>() + .first() + .map(|r| r.to_owned()) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReleaseFlavor { Stable, BleedingEdge, } +impl Display for ReleaseFlavor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReleaseFlavor::Stable => write!(f, "Stable"), + ReleaseFlavor::BleedingEdge => write!(f, "Bleeding edge"), + } + } +} + impl Default for ReleaseFlavor { fn default() -> Self { Self::Stable } } -pub type BepInExAssets = HashMap; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BepInExAsset { + pub name: String, + pub link: String, +} #[derive(Debug, Clone, PartialEq, Eq)] pub struct BepInExRelease { pub version: Version, - pub assets: BepInExAssets, + pub assets: Vec, pub flavor: ReleaseFlavor, } +impl BepInExRelease { + pub fn select_asset(&self, query: String) -> Option { + self.assets + .iter() + .find(|a| a.name == query) + .map(|a| a.to_owned()) + } + + pub fn to_query(&self, game: &Game) -> String { + match self.flavor { + ReleaseFlavor::Stable => match self.version.major { + 6 => format!( + "BepInEx_{}_{}_{}.zip", + game_type!(game.ty), + game.arch, + &self.version + ), + _ => format!("BepInEx_{}_{}.0.zip", game.arch, self), + }, + ReleaseFlavor::BleedingEdge => { + let artifact_id = self + .version + .pre + .split('.') + .filter_map(|e| e.parse::().ok()) + .collect::>()[0]; + match artifact_id >= 600 { + true => format!( + "BepInEx-{}-win-{}-{}.zip", + game.ty.as_ref().unwrap(), + game.arch, + self.version, + ), + false => { + format!( + "BepInEx_{}_{}_{}_{}.zip", + game_type!(game.ty), + game.arch, + self.version.build, + self.version.mmpp() + ) + } + } + } + } + } +} + impl From for BepInExRelease { - fn from(res: GitHubRelease) -> Self { + fn from(rel: GitHubRelease) -> Self { Self { - version: res.tag_name, - assets: res + version: rel.tag_name, + assets: rel .assets .into_iter() - .map(|asset| (asset.name, asset.browser_download_url)) + .map(|r| BepInExAsset { + name: r.name, + link: r.browser_download_url, + }) .collect(), flavor: ReleaseFlavor::Stable, } } } +impl From for BepInExRelease { + fn from(rel: BuildsRelease) -> Self { + Self { + version: rel.version, + assets: rel + .assets + .into_iter() + .map(|r| BepInExAsset { + name: r.name, + link: r.link, + }) + .collect(), + flavor: ReleaseFlavor::BleedingEdge, + } + } +} + impl Display for BepInExRelease { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.version) @@ -53,15 +149,13 @@ impl Display for BepInExRelease { } pub trait AssetDownloader { - fn download(&self, query: String, game: &Game) -> anyhow::Result<()>; + fn download(&self, game: &Game) -> anyhow::Result<()>; } -impl AssetDownloader for BepInExAssets { - fn download(&self, query: String, game: &Game) -> anyhow::Result<()> { - let asset = self.get(&query).expect("Asset not found"); - +impl AssetDownloader for BepInExAsset { + fn download(&self, game: &Game) -> anyhow::Result<()> { let client = reqwest::blocking::Client::new(); - let resp = client.get(asset).send()?.bytes()?; + let resp = client.get(&self.link).send()?.bytes()?; let content = Cursor::new(resp.to_vec()); ZipArchive::new(content)?.extract(&game.path)?; diff --git a/bepinex_sources/src/builds.rs b/bepinex_sources/src/builds.rs new file mode 100644 index 0000000..bf1b5f1 --- /dev/null +++ b/bepinex_sources/src/builds.rs @@ -0,0 +1,102 @@ +use lazy_static::lazy_static; +use regex::Regex; +use scraper::{Html, Selector}; +use semver::Version; + +use crate::{ + models::bleeding_edge::builds::{BuildsAsset, BuildsRelease}, + s_parse, select, +}; + +lazy_static! { + static ref VERISON_REGEX: Regex = Regex::new( + r"((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-(?:(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))))", + ).unwrap(); +} + +pub struct BuildsApi { + base_url: String, + min_build_id: Option, +} + +impl BuildsApi { + pub fn new(base_url: &str) -> Self { + BuildsApi { + base_url: base_url.into(), + min_build_id: None, + } + } + + pub fn set_base_url(&mut self, base_url: String) -> &mut Self { + self.base_url = base_url; + self + } + + pub fn set_min_build_id(&mut self, min_build: Option) -> &mut Self { + self.min_build_id = min_build; + self + } + + pub fn filter_builds(&self, build: &BuildsRelease) -> bool { + match &self.min_build_id { + Some(build_id) => build.artifact_id >= *build_id, + None => true, + } + } + + pub fn get_builds(&self) -> anyhow::Result> { + let mut releases: Vec = Vec::new(); + + let resp = reqwest::blocking::Client::new() + .get(format!("{}/projects/bepinex_be", &self.base_url)) + .send()?; + let html = resp.text()?; + let fragment = Html::parse_fragment(&html); + + let main_selector = s_parse!("main"); + let artifact_item_selector = s_parse!("div.artifact-item"); + let build_id_selector = s_parse!("span.artifact-id"); + let artifact_hash_selector = s_parse!("a.hash-button"); + + let artifacts_list_selector = s_parse!("div.artifacts-list"); + let artifact_link_selector = s_parse!("a.artifact-link"); + + let main = select!(fragment, &main_selector); + for el in main.select(&artifact_item_selector) { + let artifact_id = select!(el, &build_id_selector) + .text() + .filter_map(|e| e[1..].parse::().ok()) + .collect::>()[0]; + let build_hash = select!(el, &artifact_hash_selector) + .text() + .collect::>()[0] + .to_string(); + let mut version: String = "".into(); + + let mut assets: Vec = Vec::new(); + let artifacts_list = select!(el, &artifacts_list_selector); + for artifact_el in artifacts_list.select(&artifact_link_selector) { + let download_link = artifact_el.value().attr("href").unwrap(); + + let artifact_name = artifact_el.text().collect::>()[0].to_string(); + if version.is_empty() && let Some(version_m) = VERISON_REGEX.find(&artifact_name) { + version = format!("{}+{}", artifact_name[version_m.start()..version_m.end()].to_owned(), build_hash); + } + assets.push(BuildsAsset { + name: artifact_name, + link: format!("{}{}", self.base_url, download_link), + }); + } + releases.push(BuildsRelease { + artifact_id, + version: Version::parse(&version).unwrap(), + assets, + }); + } + + Ok(releases + .into_iter() + .filter(|b| self.filter_builds(b)) + .collect()) + } +} diff --git a/bepinex_sources/src/lib.rs b/bepinex_sources/src/lib.rs index a2d082f..dd86e46 100644 --- a/bepinex_sources/src/lib.rs +++ b/bepinex_sources/src/lib.rs @@ -1,3 +1,6 @@ pub mod bepinex; +pub mod builds; pub mod github; +pub mod macros; pub mod models; +pub mod version; diff --git a/bepinex_sources/src/macros.rs b/bepinex_sources/src/macros.rs new file mode 100644 index 0000000..4e053f0 --- /dev/null +++ b/bepinex_sources/src/macros.rs @@ -0,0 +1,13 @@ +#[macro_export] +macro_rules! s_parse { + ($sel:expr) => { + Selector::parse($sel).unwrap() + }; +} + +#[macro_export] +macro_rules! select { + ($elem:expr, $sel:expr) => { + $elem.select($sel).next().unwrap() + }; +} diff --git a/bepinex_sources/src/models/bleeding_edge/builds.rs b/bepinex_sources/src/models/bleeding_edge/builds.rs new file mode 100644 index 0000000..b5bcc70 --- /dev/null +++ b/bepinex_sources/src/models/bleeding_edge/builds.rs @@ -0,0 +1,14 @@ +use semver::Version; + +#[derive(Debug)] +pub struct BuildsAsset { + pub name: String, + pub link: String, +} + +#[derive(Debug)] +pub struct BuildsRelease { + pub artifact_id: usize, + pub version: Version, + pub assets: Vec, +} diff --git a/bepinex_sources/src/models/bleeding_edge/mod.rs b/bepinex_sources/src/models/bleeding_edge/mod.rs index e69de29..875fab0 100644 --- a/bepinex_sources/src/models/bleeding_edge/mod.rs +++ b/bepinex_sources/src/models/bleeding_edge/mod.rs @@ -0,0 +1 @@ +pub mod builds; diff --git a/bepinex_sources/src/models/github/releases.rs b/bepinex_sources/src/models/github/releases.rs index b64c5fa..e611411 100644 --- a/bepinex_sources/src/models/github/releases.rs +++ b/bepinex_sources/src/models/github/releases.rs @@ -1,8 +1,10 @@ use std::fmt; -use semver::{BuildMetadata, Prerelease, Version}; +use semver::Version; use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +use crate::version::VersionExt; + #[derive(Debug, Serialize, Deserialize)] pub struct GitHubAsset { pub name: String, @@ -18,16 +20,6 @@ pub struct GitHubRelease { pub assets: Vec, } -fn fix_version(major: u64, minor: u64) -> Version { - Version { - major, - minor, - patch: 0, - pre: Prerelease::EMPTY, - build: BuildMetadata::EMPTY, - } -} - fn try_to_parse(version: &str) -> Version { // Greatest fix, until ErrorKind is hidden from external crates, can't really do much let ver: Vec = version @@ -35,7 +27,7 @@ fn try_to_parse(version: &str) -> Version { .map(|e| e.parse::().unwrap_or_default()) .collect(); - Version::parse(version).unwrap_or_else(|_| fix_version(ver[0], ver[1])) + Version::parse(version).unwrap_or_else(|_| Version::fix_version(ver[0], ver[1])) } fn parse_tag<'de, D>(deserializer: D) -> Result diff --git a/bepinex_sources/src/version.rs b/bepinex_sources/src/version.rs new file mode 100644 index 0000000..9cd858d --- /dev/null +++ b/bepinex_sources/src/version.rs @@ -0,0 +1,35 @@ +use semver::{BuildMetadata, Prerelease, Version}; + +pub trait VersionExt { + fn mmp(&self) -> String; + fn mmpp(&self) -> String; + fn display(&self) -> String; + fn fix_version(major: u64, minor: u64) -> Version; +} + +impl VersionExt for Version { + fn mmp(&self) -> String { + format!("{}.{}.{}", self.major, self.minor, self.patch) + } + + fn mmpp(&self) -> String { + format!("{}.{}.{}-{}", self.major, self.minor, self.patch, self.pre) + } + + fn display(&self) -> String { + match self.pre.is_empty() { + true => self.mmp(), + false => self.mmpp(), + } + } + + fn fix_version(major: u64, minor: u64) -> Version { + Version { + major, + minor, + patch: 0, + pre: Prerelease::EMPTY, + build: BuildMetadata::EMPTY, + } + } +}