15 Commits

12 changed files with 211 additions and 65 deletions

View File

@@ -20,8 +20,8 @@ It enhances the osu! experience with quality-of-life features and integration sp
- [x] Shows the number of imported beatmapsets
- [x] Shows the number of imported skins
- [x] Displays ping to the EZPPFarm server
- [ ] Discord Rich Presence (planned and in development)
- [ ] Performance Display Overlay (planned)
- [x] Discord Rich Presence
- [x] Performance Display Overlay (currently in experimental patcher release stream)
---
@@ -31,9 +31,9 @@ It enhances the osu! experience with quality-of-life features and integration sp
| -------- | ---------------- |
| Windows | ✅ Supported |
| macOS | ❌ Not supported |
| Linux | ❌ Not supported |
| Linux | 🕧 Partially supported (via osu-winello) |
> Currently, only **Windows** is supported. Support for other platforms may be considered in the future.
> Currently, only **Windows** is fully supported. Support for other platforms may be considered in the future.
---

View File

@@ -7,7 +7,7 @@
"@better-fetch/fetch": "^1.1.18",
"@fontsource/sora": "^5.2.6",
"@fontsource/space-mono": "^5.2.8",
"@iarna/toml": "^3.0.0",
"@iarna/toml": "2.2.5",
"@number-flow/svelte": "^0.3.9",
"@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.1.11",
@@ -158,7 +158,7 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iarna/toml": ["@iarna/toml@3.0.0", "", {}, "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q=="],
"@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
"@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="],

View File

@@ -21,7 +21,7 @@
"@better-fetch/fetch": "^1.1.18",
"@fontsource/sora": "^5.2.6",
"@fontsource/space-mono": "^5.2.8",
"@iarna/toml": "^3.0.0",
"@iarna/toml": "2.2.5",
"@number-flow/svelte": "^0.3.9",
"@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.1.11",

10
src-tauri/Cargo.lock generated
View File

@@ -1094,8 +1094,9 @@ dependencies = [
[[package]]
name = "ezpplauncher"
version = "3.0.0-beta.4"
version = "3.0.1"
dependencies = [
"base64 0.21.7",
"discord-rich-presence",
"hardware-id",
"md5",
@@ -1115,6 +1116,7 @@ dependencies = [
"tauri-plugin-single-instance",
"tauri-plugin-sql",
"tokio",
"widestring",
"winapi",
"windows-sys 0.60.2",
"winreg 0.55.0",
@@ -5798,6 +5800,12 @@ dependencies = [
"wasite",
]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -1,6 +1,6 @@
[package]
name = "ezpplauncher"
version = "3.0.0-beta.4"
version = "3.0.1"
description = "EZPPLauncher redefined."
authors = ["HorizonCode"]
edition = "2024"
@@ -39,7 +39,9 @@ once_cell = "1.21.3"
[target.'cfg(windows)'.dependencies]
winreg = "0.55.0"
winapi = { version = "0.3", features = ["winuser"] }
winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
base64 = "0.21"
widestring = "1.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.0"

View File

@@ -13,8 +13,9 @@ use tokio::time::{Duration, sleep};
use crate::presence;
use crate::utils::{
check_folder_completeness, get_osu_config, get_osu_user_config, get_window_title_by_pid,
set_osu_config_vals, set_osu_user_config_vals, is_wmctrl_available, is_osuwinello_available
check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config,
get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available,
set_osu_config_vals, set_osu_user_config_vals,
};
#[tauri::command]
@@ -336,10 +337,8 @@ pub async fn run_osu_updater(folder: String) -> Result<(), String> {
sleep(Duration::from_millis(500)).await;
}
// Wait for updater process to fully exit
let _ = updater_process.wait().await;
// Clean up update-related files
let force_update_files = [".require_update", "help.txt", "_pending"];
for update_file_name in &force_update_files {
let path = PathBuf::from(&folder).join(update_file_name);
@@ -435,7 +434,7 @@ pub struct UpdateFile {
pub async fn get_ezpp_launcher_update_files(
folder: String,
update_url: String,
update_stream: String
update_stream: String,
) -> Result<(Vec<UpdateFile>, Vec<UpdateFile>), String> {
let osu_path = PathBuf::from(folder);
let client = Client::new();
@@ -536,7 +535,6 @@ pub async fn download_ezpp_launcher_update_files(
.await
.map_err(|e| e.to_string())?;
// Emit progress to frontend
app.emit(
"download-progress",
UpdateStatus {
@@ -716,3 +714,26 @@ pub fn has_wmctrl() -> bool {
pub fn has_osuwinello() -> bool {
is_osuwinello_available()
}
#[tauri::command]
pub async fn has_net8() -> bool {
is_net8_installed().await
}
#[cfg(windows)]
#[tauri::command]
pub fn encrypt_string(string: String, entropy: String) -> String {
let encrypted = encrypt_password(&string, &entropy);
match encrypted {
Ok(encrypted_vec) => encrypted_vec,
Err(_) => string,
}
}
// NOTE: should not be called by tauri on non windows systems, return the string nonthenless
#[cfg(not(windows))]
#[tauri::command]
pub fn encrypt_string(string: String, _entropy: String) -> String {
string
}

View File

@@ -5,17 +5,17 @@ pub mod commands;
pub mod presence;
pub mod utils;
use crate::commands::{
check_for_corruption, download_ezpp_launcher_update_files, exit, find_osu_installation,
get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid, get_launcher_version,
get_osu_release_stream, get_osu_skin, get_osu_version, get_platform, get_skins_count,
is_osu_running, open_url_in_browser, presence_connect, presence_disconnect,
presence_is_connected, presence_update_status, presence_update_user, replace_ui_files, run_osu,
run_osu_updater, set_osu_config_values, set_osu_user_config_values, valid_osu_folder, has_osuwinello, has_wmctrl
check_for_corruption, download_ezpp_launcher_update_files, encrypt_string, exit,
find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid,
get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform,
get_skins_count, has_net8, has_osuwinello, has_wmctrl, is_osu_running, open_url_in_browser,
presence_connect, presence_disconnect, presence_is_connected, presence_update_status,
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
set_osu_user_config_values, valid_osu_folder,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
#[cfg(target_os = "linux")]
unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
@@ -26,9 +26,6 @@ pub fn run() {
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
let app_window = app.get_webview_window("main").expect("no main window");
app_window
.set_always_on_top(true)
.expect("failed to set always on top");
app_window.set_focus().expect("failed to focus");
}));
}
@@ -62,7 +59,9 @@ pub fn run() {
presence_update_user,
presence_is_connected,
has_osuwinello,
has_wmctrl
has_wmctrl,
has_net8,
encrypt_string
])
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())

View File

@@ -1,6 +1,5 @@
use std::fs;
use std::path::Path;
use std::process::Command;
use sysinfo::Pid;
pub fn check_folder_completeness<P: AsRef<Path>>(folder_path: P, required_files: &[&str]) -> f32 {
@@ -72,7 +71,6 @@ pub fn set_osu_user_config_vals(
}
}
// Collect indices and keys to update to avoid borrow checker issues
let mut updates = Vec::new();
for (i, line) in lines.iter().enumerate() {
if let Some((existing_key, _)) = line.split_once(" = ") {
@@ -87,7 +85,6 @@ pub fn set_osu_user_config_vals(
keys_to_add.remove(trimmed_key.as_str());
}
// Add new keys that were not found
for key in keys_to_add {
if let Some(value) = keys_to_set.get(key) {
lines.push(format!("{} = {}", key, value));
@@ -140,7 +137,6 @@ pub fn set_osu_config_vals(
keys_to_add.remove(trimmed_key.as_str());
}
// Add new keys that were not found
for key in keys_to_add {
if let Some(value) = keys_to_set.get(key) {
lines.push(format!("{} = {}", key, value));
@@ -179,12 +175,13 @@ pub fn get_osu_config<P: AsRef<Path>>(
#[cfg(not(windows))]
pub fn is_osuwinello_available() -> bool {
use std::process::Command;
Command::new("osu-wine")
.arg("--info") // A lightweight operation like getting the version is ideal.
.stdout(std::process::Stdio::null()) // Suppress stdout
.stderr(std::process::Stdio::null()) // Suppress stderr
.status() // Execute the command and get its status
.is_ok() // is_ok() will be true if the command was found and ran
.arg("--info")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[cfg(windows)]
@@ -194,12 +191,13 @@ pub fn is_osuwinello_available() -> bool {
#[cfg(not(windows))]
pub fn is_wmctrl_available() -> bool {
use std::process::Command;
Command::new("wmctrl")
.arg("-V") // A lightweight operation like getting the version is ideal.
.stdout(std::process::Stdio::null()) // Suppress stdout
.stderr(std::process::Stdio::null()) // Suppress stderr
.status() // Execute the command and get its status
.is_ok() // is_ok() will be true if the command was found and ran
.arg("-V")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[cfg(windows)]
@@ -209,27 +207,24 @@ pub fn is_wmctrl_available() -> bool {
#[cfg(not(windows))]
pub fn get_window_title_by_pid(target_pid: Pid) -> String {
use std::process::Command;
let find_title = || -> Option<String> {
// 1. Execute `wmctrl -lp`
let output = Command::new("wmctrl").arg("-lp").output().ok()?;
if !output.status.success() {
return None; // wmctrl command failed (e.g., not on X11)
return None;
}
let output_str = String::from_utf8(output.stdout).ok()?;
// 2. Parse the output line by line
for line in output_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
// The PID is typically the 3rd column (index 2)
if let Ok(pid) = parts[2].parse::<u32>() {
if pid == target_pid.as_u32() {
// 3. Extract the title and return it as Some(title)
let title = parts[4..].join(" ");
return Some(title);
}
@@ -244,13 +239,13 @@ pub fn get_window_title_by_pid(target_pid: Pid) -> String {
#[cfg(windows)]
pub fn get_window_title_by_pid(pid: Pid) -> String {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::sync::{Arc, Mutex};
use winapi::shared::windef::HWND;
use winapi::um::winuser::{
EnumWindows, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
};
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
extern "system" fn enum_windows_proc(
hwnd: HWND,
@@ -271,11 +266,11 @@ pub fn get_window_title_by_pid(pid: Pid) -> String {
let title_str = title.to_string_lossy().into_owned();
if !title_str.is_empty() {
*result.lock().unwrap() = Some(title_str);
return 0; // Stop enumeration
return 0;
}
}
}
1 // Continue enumeration
1
}
}
@@ -289,3 +284,92 @@ pub fn get_window_title_by_pid(pid: Pid) -> String {
}
result.lock().unwrap().clone().unwrap_or_default()
}
pub async fn is_net8_installed() -> bool {
use std::process::Command;
let output_result = Command::new("dotnet").arg("--list-runtimes").output();
match output_result {
Ok(output) => {
if !output.status.success() {
eprintln!(
"Error: `dotnet --list-runtimes` failed with status: {}",
output.status
);
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
return false;
}
let stdout_str = String::from_utf8_lossy(&output.stdout);
stdout_str
.lines()
.any(|line| line.starts_with("Microsoft.WindowsDesktop.App 8."))
}
Err(_) => false,
}
}
#[cfg(windows)]
pub fn encrypt_password(password: &str, entropy: &str) -> Result<String, String> {
use base64::{Engine as _, engine::general_purpose};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
use std::slice;
use winapi::shared::minwindef::{BYTE, DWORD, LPVOID};
use winapi::shared::ntdef::LPCWSTR;
use winapi::um::dpapi::{CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData};
use winapi::um::winbase::LocalFree;
use winapi::um::wincrypt::DATA_BLOB;
let description = "Encrypted";
let password_bytes = password.as_bytes();
let mut input_blob = DATA_BLOB {
cbData: password_bytes.len() as DWORD,
pbData: password_bytes.as_ptr() as *mut BYTE,
};
let entropy_bytes = entropy.as_bytes();
let mut entropy_blob = DATA_BLOB {
cbData: entropy_bytes.len() as DWORD,
pbData: entropy_bytes.as_ptr() as *mut BYTE,
};
let mut output_blob = DATA_BLOB {
cbData: 0,
pbData: ptr::null_mut(),
};
let wide_description: Vec<u16> = OsStrExt::encode_wide(OsStr::new(description))
.chain(Some(0))
.collect();
let p_description: LPCWSTR = wide_description.as_ptr();
let result = unsafe {
CryptProtectData(
&mut input_blob,
p_description,
&mut entropy_blob,
ptr::null_mut(),
ptr::null_mut(),
CRYPTPROTECT_UI_FORBIDDEN,
&mut output_blob,
)
};
if result == 0 {
return Err("CryptProtectData failed".to_string());
}
let encrypted_data =
unsafe { slice::from_raw_parts(output_blob.pbData, output_blob.cbData as usize).to_vec() };
unsafe {
LocalFree(output_blob.pbData as LPVOID);
}
let base64_string = general_purpose::STANDARD_NO_PAD.encode(&encrypted_data);
Ok(base64_string)
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ezpplauncher",
"version": "3.0.0-beta.2",
"version": "3.0.1",
"identifier": "farm.ezpp.launcher",
"build": {
"beforeDevCommand": "bun run vite:dev",

View File

@@ -137,3 +137,7 @@ export const hasWMCTRL = async () =>
await invoke<boolean>('has_wmctrl');
export const hasOsuWinello = async () =>
await invoke<boolean>('has_osuwinello');
export const hasNet8 = async () =>
await invoke<boolean>('has_net8');
export const encryptString = async (str: string, entropy: string) =>
await invoke<string>('encrypt_string', {string: str, entropy});

View File

@@ -83,6 +83,7 @@
import { osuapi } from '@/api/osuapi';
import {
downloadEZPPLauncherUpdateFiles,
encryptString,
exit,
getBeatmapSetsCount,
getEZPPLauncherUpdateFiles,
@@ -90,6 +91,7 @@
getSkin,
getSkinsCount,
getVersion,
hasNet8,
hasOsuWinello,
hasWMCTRL,
isOsuCorrupted,
@@ -189,7 +191,7 @@
return;
}
if($platform === "linux"){
if ($platform === 'linux') {
if (!(await hasWMCTRL())) {
toast.error('Hmmm...', {
description: 'wmctrl seems to be missing, please install via AUR.',
@@ -295,7 +297,7 @@
},
{
key: 'Password',
value: password,
value: $platform === "windows" ? await encryptString(password, "cu24180ncjeiu0ci1nwui") : password,
},
{
key: 'SaveUsername',
@@ -1050,12 +1052,18 @@
>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Patching</Label>
<div class="text-muted-foreground text-xs">Shows misses in Relax and Autopilot {#if $platform !== "windows"}<span class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg">currently only on windows!</span> {/if}</div>
<div class="text-muted-foreground text-xs">
Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg"
>currently only on windows!</span
>
{/if}
</div>
</div>
<Checkbox
id="setting-custom-cursor"
checked={$platform === "windows" ? $patch : false}
disabled={$platform !== "windows"}
checked={$platform === 'windows' ? $patch : false}
disabled={$platform !== 'windows'}
onCheckedChange={async (e) => {
patch.set(e);
$userSettings.save();
@@ -1138,7 +1146,7 @@
</div>
</div>
<div
class="grid grid-cols-[0.7fr_auto] gap-y-5 items-center border-theme-800 pl-6 pr-5 pb-4"
class="grid grid-cols-[0.7fr_auto] gap-y-1 items-center border-theme-800 pl-6 pr-5 pb-4"
>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">osu! installation path</Label>
@@ -1168,8 +1176,21 @@
<div class="flex flex-row w-full">
<Select.Root
type="single"
value={$launcherStream}
bind:value={$launcherStream}
onValueChange={async (newStream) => {
if (newStream === 'experimental' && !(await hasNet8())) {
launcherStream.set('stable');
toast.error('.NET 8.0 Desktop Runtime not found!', {
action: {
label: 'Download .NET 8.0',
onClick: async () =>
await openURL(
'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.18-windows-x64-installer'
),
},
});
return;
}
$userSettings.value('patcherStream').set(newStream);
launcherStream.set(newStream);
await $userSettings.save();

View File

@@ -26,7 +26,7 @@
import { Buffer } from 'buffer';
import { Toaster } from '@/components/ui/sonner';
import { userAuth } from '@/userAuthentication';
import { exit, getLauncherVersion, getPlatform } from '@/osuUtil';
import { encryptString, exit, getLauncherVersion, getPlatform } from '@/osuUtil';
import Button from '@/components/ui/button/button.svelte';
import * as presence from '@/presence';
@@ -88,6 +88,13 @@
onMount(async () => {
window.Buffer = Buffer;
/* const decryptTest = "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAs463WdMtG0mr/mVLQCZ6dQAAAAAIAAAAUgBhAHcAAAAQZgAAAAEAACAAAABnyhFo8QK2iL5aTehKmsZSUpihGWBwlKfYj7cL2/lDagAAAAAOgAAAAAIAACAAAABMxyG6EdkLORSMB8isbltZhPQV2iVQ2r+yJLJ2Tw9yWiAAAABE1EzBPD9PRFQ3evk0vneNpfQTNTvMpjDVUTl3kAaKWUAAAADBshJzPDx6qcYfYh2zh6cKJWClZpIp6H50IKriW936XGhLFQboK/m18O77TBnpSrs0YosFjzsYVJTkrWjetFAf";
const decrypted = await decryptString(decryptTest);
console.log(decrypted); */
const encrypted = await encryptString("Todesengel007008009!", "cu24180ncjeiu0ci1nwui");
console.log(encrypted);
disableReload();
setupValues();
launcherVersion.set(await getLauncherVersion());