From bdbb32531a7a55b76b8e6f1a4fe4a3b679207d55 Mon Sep 17 00:00:00 2001 From: RealViper8 Date: Sun, 1 Mar 2026 19:52:53 +0100 Subject: [PATCH] Fix #23: slow lookup & slow remove are fixed ! --- Cargo.lock | 6 +-- Cargo.toml | 4 +- README.md | 15 +++++-- gui/main.rs | 89 ++++++++++++++++++++++++++++++++-------- gui/utils/bserializer.rs | 8 ++-- gui/window/mods.rs | 10 ++--- gui/window/view.rs | 2 - src/lib.rs | 30 +++++++++++--- 8 files changed, 122 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f80a111..533d239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,9 +1264,9 @@ dependencies = [ [[package]] name = "egui_code_editor" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1b847b0ff5a3bac4e8604eca605808970bbcdaba76b3101db1051e4206dc6" +checksum = "70addd812d64b30d2adabdc4598c648f4fb38cac5df641260b1996efdf22851f" dependencies = [ "egui", ] @@ -4330,7 +4330,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "steam" -version = "0.3.1" +version = "0.4.1" dependencies = [ "cc", "criterion", diff --git a/Cargo.toml b/Cargo.toml index b67e4d7..a70525b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Gabriel"] license = "MIT" name = "steam" -version = "0.3.1" +version = "0.4.1" edition = "2024" [lib] @@ -24,7 +24,7 @@ opt-level = 3 [dependencies] eframe = { version = "0.33.3", features = ["wgpu", "persistence"] } -egui_code_editor = "0.2.20" +egui_code_editor = "0.2.21" egui_extras = { version = "0.33.3", features = ["all_loaders"] } image = { version = "0.25.9", features = ["jpeg"] } reqwest = { version = "0.13.2", features = ["json","blocking"] } diff --git a/README.md b/README.md index a59e147..bc80f81 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🧰 Steamtools -**Steamtools** is a lightweight tool written in **Rust** that allows you to easily **view, install, and remove Lua Manifests**. The source code of the proxy dll: xinput1_4.dll is currently not available since I only ship the binary for it. May change in Future ! +**Steamtools** is a lightweight tool written in **Rust** that allows you to easily **view, install, and remove Lua Manifests**. The source code of the proxy dll: xinput1_4.dll is currently not available since I only ship the binary for it. May **change** in the **future** ! --- @@ -11,7 +11,7 @@ - [x] Add tool: Install - [x] Add tool: Uninstall - [X] Add tool: View -- [X] Add Window: Settings (experimental) +- [X] Add Window: Settings --- @@ -25,10 +25,19 @@ --- +## Donating + +Since this is a personal project and I invest my free time in making this you could support me by donating! +SOL: GYepZXvMAyQ8y54HuWx8QR3yBBg9EFEqn7dDJo93GbTM (SOL Network) + +[![Donate](https://img.shields.io/badge/Donate-Ko--fi-orange)](https://ko-fi.com/realviper) + ## Prerequisites - VC_Runtime: [x64]("https://aka.ms/vs/17/release/vc_redist.x64.exe") -- Opengl Drivers: [AMD]("https://www.amd.com/en/support/download/drivers.html") [NVIDIA]("https://www.nvidia.com/de-de/drivers/") +- Opengl Drivers: + - [AMD]("https://www.amd.com/en/support/download/drivers.html") + - [NVIDIA]("https://www.nvidia.com/de-de/drivers/") **Language:** Rust 🦀 **Status:** In active development 🚀 diff --git a/gui/main.rs b/gui/main.rs index 52b423d..3002828 100644 --- a/gui/main.rs +++ b/gui/main.rs @@ -1,6 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem="windows")] - +use std::collections::HashMap; use std::fmt::Write; use std::process; use std::{fs, path::PathBuf, process::exit, sync::{Arc, Mutex}, thread}; @@ -34,7 +34,7 @@ struct App { st: Steam, settings: Settings, state: State, - games: Arc>>, + games: Arc>>, cached_games: GameMap, loaded: bool, view: ViewPopup, @@ -121,7 +121,7 @@ impl App { }); storage_ref.get_string("games" ).map(|games| { - app.games = serde_json::from_str::>>>(&games).unwrap(); + app.games = serde_json::from_str::>>>(&games).unwrap(); app.loaded = true; }); @@ -229,7 +229,10 @@ impl eframe::App for App { ui.horizontal(|ui| { ui.label("Made by"); ui.hyperlink_to("RealViper", "https://github.com/RealViper8/Steamtools"); + ui.add_space((ui.available_width()/2.0)+85.0); + ui.label(format!("Version: {}", VERSION)); }); + }); }); @@ -281,7 +284,7 @@ impl eframe::App for App { path.push(&self.st.path); path.push("config\\stplug-in"); match files { - Some(files) => { + Some(ref files) => { files.iter().for_each(|file| { path.push(&file.file_stem().unwrap()); fs::copy(file.as_path(),format!("{}.lua", &path.to_string_lossy())).unwrap(); @@ -289,6 +292,37 @@ impl eframe::App for App { }, None => () } + + if files.is_none() { + return; + } + + // for file in files.unwrap() { + // let games_lock = self.games.clone(); + // match file.file_stem().unwrap().to_str().unwrap().parse::().ok() { + // Some(appid) => { + // let appid = appid; + // thread::spawn(move || { + // let url = format!("{}{}", STEAM_URL, appid); + // println!("[FETCHING] {}", appid); + // dbg!(&url); + // let resp: HashMap = match blocking::get(url).ok().unwrap().json() { + // Ok(r) => { + // r + // }, + // Err(e) => { + // eprintln!("[FETCHING ERROR] {}", e); + // return + // } + // }; + + + // }); + // }, + // None => println!("error: failed to read .lua make sure the filename is the appid !"), + // }; + // } + self.loaded = false; } if ui.checkbox(&mut self.unlock, "Unlock").changed() { @@ -319,16 +353,20 @@ impl eframe::App for App { let height = ui.available_height(); egui::ScrollArea::vertical().auto_shrink([false; 2]).show(ui, |ui| { egui::Grid::new("games").striped(false).show(ui, |ui| { - for (i, game) in self.games.lock().unwrap().iter().enumerate() { + let t = { + self.games.lock().unwrap().clone() + }; + + for (i, game) in t.iter().enumerate() { ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { self.buffer.clear(); - write!(&mut self.buffer, "file://icons/{}.jpg", game.appid).unwrap(); + write!(&mut self.buffer, "file://icons/{}.jpg", game.0).unwrap(); ui.add( egui::Image::new(&self.buffer) .fit_to_exact_size(egui::vec2(width * 0.4, height * 0.4)) ); self.buffer.clear(); - ui.add_sized([width * 0.3, height * 0.3], egui::Label::new(RichText::new(&game.details.name).strong()).wrap()); + ui.add_sized([width * 0.3, height * 0.3], egui::Label::new(RichText::new(&game.1.details.name).strong()).wrap()); ui.vertical(|ui| { let height = ui.available_height(); @@ -337,29 +375,38 @@ impl eframe::App for App { p.push("config"); p.push("stplug-in"); self.buffer.clear(); - write!(&mut self.buffer, "{}.lua", game.appid).unwrap(); + write!(&mut self.buffer, "{}.lua", game.0).unwrap(); p.push(&self.buffer); self.buffer.clear(); - if fs::remove_file(p).is_err() { + if fs::remove_file(&p).is_err() { rfd::MessageDialog::new() .set_title("Error") .set_description("Failed to delete") .set_buttons(rfd::MessageButtons::Ok); } - self.loaded = false; + p.clear(); + p.push("icons"); + p.push(format!("{}.jpg", game.0)); + dbg!(&p); + + fs::remove_file(&p).ok(); + + if let Ok(ref mut g) = self.games.try_lock() { + g.remove(game.0); + } } - if game.installed { + if game.1.installed { if ui.add_sized([width * 0.3, height * 0.3], egui::Button::new(RichText::new("\u{1F5D1} Uninstall").strong().raised())).on_hover_text("Prompts steam to uninstall the game").clicked() { - write!(&mut self.buffer, "start steam://uninstall/{}", game.appid).unwrap(); + write!(&mut self.buffer, "start steam://uninstall/{}", game.1.appid).unwrap(); #[cfg(target_os="windows")] process::Command::new("cmd").args(["/C", &self.buffer]).spawn().expect("Failed to uninstall"); self.buffer.clear(); } } else { if ui.add_sized([width * 0.3, height * 0.3], egui::Button::new(RichText::new("\u{2795} Install").strong().raised())).on_hover_text("Prompts steam to install the game").clicked() { - write!(&mut self.buffer, "start steam://install/{}", game.appid).unwrap(); + write!(&mut self.buffer, "start steam://install/{}", game.1.appid).unwrap(); #[cfg(target_os="windows")] process::Command::new("cmd").args(["/C", &self.buffer]).spawn().expect("Failed to install"); self.buffer.clear(); @@ -367,7 +414,7 @@ impl eframe::App for App { } if ui.add_sized([width * 0.3, height * 0.3], egui::Button::new(RichText::new("\u{1F50D} View").strong())).on_hover_text("View information about the game").clicked() { - self.view.game_id = game.appid; + self.view.game_id = game.1.appid; self.view.current_game = i; self.view.active = true; } @@ -386,9 +433,17 @@ impl eframe::App for App { let s = self.st.path.clone(); let games_arc = self.games.clone(); thread::spawn(move || { - let result = get_games(&s); - let mut games_lock = games_arc.lock().unwrap(); - *games_lock = result; + let mut sbin = fs::File::create(STEAM_BINARY_PATH).unwrap(); + + let current_games = { + games_arc.lock().unwrap().clone() + }; + GameMap::write_to(&mut sbin, ¤t_games).unwrap(); + + let result = get_games(&s, current_games); + + let mut games = games_arc.lock().unwrap(); + *games = result; }); self.loaded = true; diff --git a/gui/utils/bserializer.rs b/gui/utils/bserializer.rs index 2561c61..9782eaf 100644 --- a/gui/utils/bserializer.rs +++ b/gui/utils/bserializer.rs @@ -27,7 +27,7 @@ fn read_string(reader: &mut impl Read, len_buf: &mut [u8; 4], buf: &mut Vec) pub struct GameMap(pub HashMap); impl GameMap { - pub fn write_to(file: &mut impl Write, map: HashMap) -> io::Result<()> { + pub fn write_to(file: &mut impl Write, map: &HashMap) -> io::Result<()> { let mut writer = BufWriter::new(file); writer.write_all(&(map.len() as u32).to_le_bytes())?; // Length @@ -105,11 +105,11 @@ mod tests { #[test] fn write_read() { - let mut f = fs::File::create("test.lua").unwrap(); + let mut f = fs::File::create("test.bin").unwrap(); let mut map = HashMap::new(); let g = Game::default(); - map.insert(1, &g); - let gm = GameMap::write_to(&mut f, map); + map.insert(1, g); + let gm = GameMap::write_to(&mut f, &map); gm.unwrap() } } \ No newline at end of file diff --git a/gui/window/mods.rs b/gui/window/mods.rs index a5699a5..db9a9cf 100644 --- a/gui/window/mods.rs +++ b/gui/window/mods.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; use std::fs::File; use std::path::Path; -use steamtools::{Game, install_melonloader}; +use steamtools::{install_melonloader}; use crate::STEAM_BINARY_PATH; use crate::utils::bserializer::GameMap; @@ -27,10 +26,11 @@ impl WindowPopup for ModsPopup { if ui.button("Get").clicked() { if app.cached_games.0.is_empty() && !Path::new(STEAM_BINARY_PATH).exists() { - let games_guard = app.games.lock().unwrap(); let mut stfile = File::create(STEAM_BINARY_PATH).unwrap(); - dbg!(&games_guard.iter().map(|g| (g.appid, g)).collect::>()); - GameMap::write_to(&mut stfile, games_guard.iter().map(|g| (g.appid, g)).collect::>()).unwrap(); + { + let games_guard = app.games.lock().unwrap(); + GameMap::write_to(&mut stfile, &games_guard).unwrap(); + } } else if app.cached_games.0.is_empty() && Path::new(STEAM_BINARY_PATH).exists() { let mut stfile = File::open(STEAM_BINARY_PATH).unwrap(); app.cached_games.0 = GameMap::read_from(&mut stfile).unwrap(); diff --git a/gui/window/view.rs b/gui/window/view.rs index f42fd9d..ce465b9 100644 --- a/gui/window/view.rs +++ b/gui/window/view.rs @@ -32,8 +32,6 @@ impl WindowPopup for ViewPopup { write!(&mut app.buffer, "APPID: {}", app.view.game_id).unwrap(); ui.label(&app.buffer); app.buffer.clear(); - // ui.horizontal(|ui| { - // }) }); } } diff --git a/src/lib.rs b/src/lib.rs index 74fa1fe..844e069 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ use serde::{Serialize, Deserialize}; pub mod st; // Can get ip timeouted if user requests too much !!! -const STEAM_URL: &str = "https://store.steampowered.com/api/appdetails?appids="; +pub const STEAM_URL: &str = "https://store.steampowered.com/api/appdetails?appids="; // const STEAM_HEADER_URL: &str = "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/"; @@ -34,7 +34,7 @@ pub struct AppData { // pub pc_requirements: HashMap, } -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct Game { pub appid: u32, pub details: AppData, @@ -118,7 +118,7 @@ pub fn install_melonloader(path: &str, melon_loader: bool) -> Option<()> { Some(()) } -pub fn get_games(path: impl Into + Copy) -> Vec { +pub fn get_games(path: impl Into + Copy, current_games: HashMap) -> HashMap { let mut p = path.into(); p.push("config"); p.push("stplug-in"); @@ -126,7 +126,7 @@ pub fn get_games(path: impl Into + Copy) -> Vec { let mut gp = path.into(); gp.push("steamapps"); - let mut games: Vec = Vec::new(); + let mut games: HashMap = current_games; let entries = match fs::read_dir(p) { Ok(entries) => entries, @@ -176,6 +176,18 @@ pub fn get_games(path: impl Into + Copy) -> Vec { dbg!(&installed); + let mut icons: Option> = None; + if Path::new("icons").exists() { + icons = Some(HashMap::new()); + + for entry in fs::read_dir("icons").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + icons.as_mut().unwrap().insert(path.file_stem().unwrap().to_str().unwrap().parse::().unwrap(), path); + } + } + 'entries: for entry in entries { let entry = match entry { Ok(e) => e, @@ -184,7 +196,7 @@ pub fn get_games(path: impl Into + Copy) -> Vec { continue; } }; - + let path = entry.path(); if path.is_file() { let appid = path.file_stem().unwrap(); @@ -197,6 +209,12 @@ pub fn get_games(path: impl Into + Copy) -> Vec { } }; + if let Some(ic) = icons.as_ref() { + if ic.contains_key(&appid_i) { + continue 'entries; + } + } + let url = format!("{}{}", STEAM_URL, appid.display()); println!("[FETCHING] {}", appid.display()); dbg!(&url); @@ -210,7 +228,7 @@ pub fn get_games(path: impl Into + Copy) -> Vec { let installed_val: bool = installed.contains_key(&appid_i); - games.push(Game { + games.insert(appid_i, Game { appid: appid.to_string_lossy().to_string().parse::().unwrap(), details: if let Some(r) = resp.get(&appid.to_string_lossy().to_string()) && let Some(data) = &r.data{ if !Path::new(&format!("icons/{}.jpg", appid.display())).exists() {