Compare commits

..

2 Commits
main ... gui

Author SHA1 Message Date
88f0c01cf5 remove emojis 2026-03-18 01:05:03 +00:00
1c23da5db3 Add project README with features and credits 2026-03-18 04:04:02 +03:00
70 changed files with 850 additions and 1467 deletions

View File

@ -1,42 +0,0 @@
name: Build & Release
on:
push:
branches:
- main
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: windows-cargo-${{ hashFiles('**/Cargo.lock') }}
- run: npm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
CARGO_BUILD_JOBS: "2"
with:
tagName: ${{ github.ref_name }}
releaseName: RLidentity ${{ github.ref_name }}
releaseBody: New release
releaseDraft: false

66
.gitignore vendored
View File

@ -22,69 +22,3 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Environment
.env
.env.*
# Node / package managers
package-lock.json
pnpm-lock.yaml
yarn.lock
.turbo
# Vite / build caches
.vite/
.cache/
dist/
build/
/node_modules/
# TypeScript
*.tsbuildinfo
# Tauri / Rust
src-tauri/target/
target/
/dist/tauri
# CMake / build artifacts
build/
build-x64/
cmake-build-debug/
CMakeFiles/
bin/
lib/
Debug/
Release/
*.obj
*.o
*.lib
*.a
*.so
*.dylib
*.pdb
*.ilk
*.key
*.pvk
*.pfx
# Visual Studio
.vs/
bin/
obj/
*.user
*.userosscache
*.VC.db
*.VC.VC.opendb
*.cer
# IDEs
.idea/
*.sublime-workspace
# Misc
.DS_Store
Thumbs.db
# Logs again (catch-all)
*.log

View File

@ -1,12 +1,10 @@
# RLidentity v2.0.1 # RLidentity v2.0.0
> **Be anyone, Win everything.** > **Be anyone, Win everything.**
![App Logo](https://cdn.discordapp.com/icons/1470914465515049083/88f78baaa66b440109a59a7999951cd8.webp?size=128) RLidentity is a modern, high-performance identity management tool for Rocket League. It features a sleek, glass-morphism GUI built with Tauri and React, backed by a powerful C++ injection system.
RLidentity is a modern, high-performance Name Spoofing tool for Rocket League. It features a sleek, glass-morphism GUI built with Tauri and React, backed by a powerful C++ injection system.
![App Logo](public/rlidentity.webp)
## Features ## Features
@ -44,7 +42,10 @@ The DLL and Injector are built using Visual Studio 2022 (v143 toolset). Ensure `
* **Lead Dev & Owner**: Bits * **Lead Dev & Owner**: Bits
* **Dev & Admin**: Danni * **Dev & Admin**: Danni
* **Co-Owner**: Deniz
* **Administrator**: Kairo
* **Helpers**: Quinn, sndr * **Helpers**: Quinn, sndr
* **Tester**: Emir
## 🔗 Links ## 🔗 Links

Binary file not shown.

Binary file not shown.

BIN
logo.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,7 +1,7 @@
{ {
"name": "RLIdentity", "name": "rlidentitygui",
"private": true, "private": true,
"version": "2.0.1", "version": "2.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

16
src-tauri/.gitignore vendored
View File

@ -1,19 +1,3 @@
# src-tauri (Rust / Tauri) ignores
# Rust
/target/
**/target/
# Tauri build artifacts
/.tauri/build/
/.tauri/bundle/
# Editor
.vscode/
.idea/
# Logs
*.log
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target/ /target/

38
src-tauri/Cargo.lock generated
View File

@ -2,26 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "RLIdentity"
version = "2.0.1"
dependencies = [
"dirs 5.0.1",
"hex",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-updater",
"tokio",
"window-vibrancy 0.5.3",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@ -3472,6 +3452,24 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rlidentitygui"
version = "2.0.0"
dependencies = [
"dirs 5.0.1",
"reqwest 0.12.28",
"serde",
"serde_json",
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-updater",
"tokio",
"window-vibrancy 0.5.3",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"

View File

@ -1,8 +1,8 @@
[package] [package]
name = "RLIdentity" name = "rlidentitygui"
version = "2.0.1" version = "2.0.0"
description = "check out https://rlidentity.me for more info!" description = "A Tauri App"
authors = ["bits", "danni :3"] authors = ["you"]
edition = "2021" edition = "2021"
[lib] [lib]
@ -13,8 +13,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = '0.12', features = ['json'] }
tokio = { version = "1", features = ["full"] } tokio = { version = '1', features = ['full'] }
window-vibrancy = "0.5.2"
tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -23,7 +24,4 @@ sysinfo = "0.30"
dirs = "5" dirs = "5"
tauri-plugin-updater = "2.10.0" tauri-plugin-updater = "2.10.0"
tauri-plugin-process = "2.3.1" tauri-plugin-process = "2.3.1"
sha2 = "0.10"
hex = "0.4"
[target.'cfg(target_os = "windows")'.dependencies]
window-vibrancy = "0.5.2"

View File

@ -14,6 +14,6 @@
"opener:default", "opener:default",
"updater:default", "updater:default",
"process:allow-restart", "process:allow-restart",
"allow-main-commands" "default"
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,16 +0,0 @@
{
"identifier": "allow-main-commands",
"description": "Allows RLIdentity core commands",
"commands": {
"allow": [
"minimize_to_tray",
"save_config",
"inject_dll",
"validate_key",
"check_status",
"get_hwid",
"get_app_version",
"download_assets"
]
}
}

View File

@ -13,6 +13,5 @@ commands.allow = [
"inject_dll", "inject_dll",
"validate_key", "validate_key",
"check_status", "check_status",
"get_hwid", "get_hwid"
"download_assets"
] ]

View File

@ -1,15 +1,11 @@
use reqwest::Client; use reqwest;
use serde::{Deserialize, Serialize}; use tauri::{Manager, WebviewWindow};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
use tokio::fs;
use tauri::{Manager, State, WebviewWindow};
use window_vibrancy::apply_acrylic; use window_vibrancy::apply_acrylic;
use sysinfo::{ProcessRefreshKind, RefreshKind, System}; use serde::Serialize;
use sha2::{Sha256, Digest}; use std::fs;
use sysinfo::System;
// --- types --- use std::process::Command;
use std::path::Path;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Status { pub struct Status {
@ -17,245 +13,245 @@ pub struct Status {
pub is_injected: bool, pub is_injected: bool,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize)]
pub struct UserInfo { pub struct UserInfo {
#[serde(rename = "userId")] pub userId: Option<String>,
pub user_id: Option<String>, pub discordId: Option<String>,
#[serde(rename = "discordId")] pub epicId: Option<String>,
pub discord_id: Option<String>,
#[serde(rename = "epicId")]
pub epic_id: Option<String>,
pub username: Option<String>, pub username: Option<String>,
#[serde(rename = "globalName")] pub globalName: Option<String>,
pub global_name: Option<String>,
pub logins: Option<i32>, pub logins: Option<i32>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize)]
pub struct KeyValidationResponse { pub struct KeyValidationResponse {
pub status: String, pub status: String,
pub user: Option<UserInfo>, pub user: Option<UserInfo>,
} }
#[derive(Serialize, Deserialize)] #[tauri::command]
struct SaveConfigPayload { fn minimize_to_tray(window: WebviewWindow) {
#[serde(rename = "spoofedName")] let _ = window.hide();
name: String,
platform: String,
} }
#[derive(Deserialize)] #[tauri::command]
struct AssetManifest { fn get_hwid() -> String {
injector_hash: String, // Simple HWID using Windows UUID
dll_hash: String, let output = Command::new("wmic")
.args(["csproduct", "get", "uuid"])
.output()
.ok();
if let Some(out) = output {
let s = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = s.lines().collect();
if lines.len() >= 2 {
return lines[1].trim().to_string();
}
}
"UNKNOWN-HWID".to_string()
} }
struct AppState { fn get_last_epic_id() -> String {
client: Client, if let Some(mut path) = dirs::data_dir() {
app_data: PathBuf, path.push("RLidentity");
sys: Mutex<System>, path.push("last_epic_id.txt");
}
// --- helpers --- if let Ok(content) = fs::read_to_string(path) {
let trimmed = content.trim();
async fn get_last_epic_id(base_path: &Path) -> String { if trimmed.len() == 32 {
let path = base_path.join("last_epic_id.txt"); return trimmed.to_string();
if let Ok(content) = fs::read_to_string(path).await { }
let trimmed = content.trim();
if trimmed.len() == 32 {
return trimmed.to_string();
} }
} }
"".to_string() "".to_string()
} }
// --- commands --- #[tauri::command]
async fn validate_key(key: String, hwid: String) -> Result<KeyValidationResponse, String> {
let epic_id = get_last_epic_id();
// Added epicId query parameter to the URL
let url = format!(
"https://api.rlidentity.me/keys/{}?hwid={}&epicId={}",
key, hwid, epic_id
);
#[tauri::command(rename_all = "snake_case")] let client = reqwest::Client::builder()
fn get_hwid() -> String { .danger_accept_invalid_certs(true)
let output = Command::new("reg") .build()
.args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"]) .map_err(|e| format!("Client Error: {}", e))?;
.output();
if let Ok(out) = output { println!("[LOG] Connecting to: {}", url);
let s = String::from_utf8_lossy(&out.stdout); println!("[LOG] Sending Epic ID: {}", epic_id);
if let Some(guid) = s.split_whitespace().last() {
if guid.len() == 36 && guid.contains('-') {
return guid.to_string();
}
}
}
"00000000-0000-0000-0000-000000000000".to_string()
}
#[tauri::command(rename_all = "snake_case")] let res = client.get(&url)
async fn validate_key(
key: String,
hwid: String,
state: State<'_, AppState>
) -> Result<KeyValidationResponse, String> {
let epic_id = get_last_epic_id(&state.app_data).await;
let url = format!("https://api.rlidentity.me/keys/{}?hwid={}&epicId={}", key, hwid, epic_id);
let res = state.client.get(&url)
.send() .send()
.await .await
.map_err(|e| format!("network error: {}", e))?; .map_err(|e| {
let err_msg = format!("Network Error: {}. Is the server on 443?", e);
println!("[ERROR] {}", err_msg);
err_msg
})?;
res.json::<KeyValidationResponse>() println!("[LOG] HTTP Status: {}", res.status());
.await
.map_err(|e| format!("api schema mismatch: {}", e)) let json: serde_json::Value = res.json().await.map_err(|e| {
let err_msg = format!("JSON Parse Error: {}", e);
println!("[ERROR] {}", err_msg);
err_msg
})?;
println!("[LOG] Server Payload: {:?}", json);
let status = json.get("status").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let user = json.get("user").map(|u| UserInfo {
userId: u.get("userId").and_then(|v| {
v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string()))
}),
discordId: u.get("discordId").and_then(|v| v.as_str()).map(|s| s.to_string()),
epicId: u.get("epicId").and_then(|v| v.as_str()).map(|s| s.to_string()),
username: u.get("username").and_then(|v| v.as_str()).map(|s| s.to_string()),
globalName: u.get("globalName").and_then(|v| v.as_str()).map(|s| s.to_string()),
logins: u.get("logins").and_then(|v| v.as_i64()).map(|n| n as i32),
});
Ok(KeyValidationResponse { status, user })
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command]
async fn inject_dll(state: State<'_, AppState>) -> Result<String, String> { async fn save_config(name: String, platform: String) -> Result<(), String> {
let injector_path = state.app_data.join("injector.exe"); let mut path = dirs::data_dir().ok_or("Could not find AppData")?;
let dll_path = state.app_data.join("RLIdentity.dll"); path.push("RLidentity");
fs::create_dir_all(&path).map_err(|e| e.to_string())?;
path.push("config.json");
let is_running = { let json = serde_json::json!({
let mut sys = state.sys.lock().unwrap(); "spoofedName": name,
sys.refresh_processes_specifics(ProcessRefreshKind::new()); "platform": platform
let x = sys.processes_by_exact_name("RocketLeague.exe").next().is_some(); x });
};
if !is_running { fs::write(path, serde_json::to_string_pretty(&json).unwrap()).map_err(|e| e.to_string())?;
return Err("rocket league is not running".into()); Ok(())
}
if !injector_path.exists() || !dll_path.exists() {
return Err("files missing, please update".into());
}
let output = Command::new(injector_path)
.arg("RocketLeague.exe")
.arg(&dll_path)
.output()
.map_err(|e| format!("exec failed: {}", e))?;
if output.status.success() {
Ok("injected".into())
} else {
Err("injection failed: check admin privileges".into())
}
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command]
async fn download_assets(state: State<'_, AppState>) -> Result<(), String> { async fn check_status() -> Status {
fs::create_dir_all(&state.app_data).await.map_err(|e| e.to_string())?; let mut s = System::new_all();
s.refresh_processes();
let is_running = s.processes_by_exact_name("RocketLeague.exe").next().is_some();
Status { is_running, is_injected: false }
}
let manifest: AssetManifest = state.client.get("https://api.rlidentity.me/manifest") #[tauri::command]
.send().await.map_err(|e| e.to_string())? async fn download_assets() -> Result<(), String> {
.json().await.map_err(|e| e.to_string())?; let mut path = dirs::data_dir().ok_or("Could not find AppData")?;
path.push("RLidentity");
fs::create_dir_all(&path).map_err(|e| e.to_string())?;
let client = reqwest::Client::new();
let assets = [ let assets = [
("injector.exe", "https://git.rlidentity.me/bits/RLidentity/raw/branch/main/injector.exe", manifest.injector_hash), ("injector.exe", "https://git.rlidentity.me/bits/RLidentity/raw/branch/dll/injector.exe"),
("RLIdentity.dll", "https://git.rlidentity.me/bits/RLidentity/raw/branch/main/RLIdentity.dll", manifest.dll_hash), ("RLIdentity.dll", "https://git.rlidentity.me/bits/RLidentity/raw/branch/dll/RLIdentity.dll"),
]; ];
for (name, url, expected_hash) in assets { for (name, url) in assets {
let file_path = state.app_data.join(name); let mut file_path = path.clone();
let res = state.client.get(url).send().await.map_err(|e| e.to_string())?; file_path.push(name);
let bytes = res.bytes().await.map_err(|e| e.to_string())?;
let mut hasher = Sha256::new(); let response = client.get(url).send().await.map_err(|e| e.to_string())?;
hasher.update(&bytes); let bytes = response.bytes().await.map_err(|e| e.to_string())?;
let actual_hash = hex::encode(hasher.finalize()); fs::write(file_path, bytes).map_err(|e| e.to_string())?;
if actual_hash != expected_hash {
return Err(format!("integrity check failed for {}", name));
}
fs::write(file_path, bytes).await.map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
} }
#[tauri::command(rename_all = "snake_case")] #[tauri::command]
async fn check_status(state: State<'_, AppState>) -> Result<Status, String> { async fn inject_dll(_discordId: Option<String>) -> Result<String, String> {
let is_running = { let mut base_path = dirs::data_dir().ok_or("Could not find AppData")?;
let mut sys = state.sys.lock().unwrap(); base_path.push("RLidentity");
sys.refresh_processes_specifics(ProcessRefreshKind::new());
let x = sys.processes_by_exact_name("RocketLeague.exe").next().is_some(); x
};
Ok(Status { is_running, is_injected: false }) let injector_path = base_path.join("injector.exe");
} let dll_path = base_path.join("RLIdentity.dll");
#[tauri::command(rename_all = "snake_case")] let mut s = System::new_all();
async fn save_config(name: String, platform: String, state: State<'_, AppState>) -> Result<(), String> { s.refresh_processes();
let config_path = state.app_data.join("config.json"); if s.processes_by_exact_name("RocketLeague.exe").next().is_none() {
let payload = SaveConfigPayload { name, platform }; return Err("Rocket League is not running!".into());
if !state.app_data.exists() {
fs::create_dir_all(&state.app_data).await.map_err(|e| e.to_string())?;
} }
let json = serde_json::to_string(&payload).map_err(|e| e.to_string())?; if !injector_path.exists() || !dll_path.exists() {
fs::write(config_path, json).await.map_err(|e| e.to_string()) return Err("Required files missing. Please wait for update to finish.".into());
} }
#[tauri::command(rename_all = "snake_case")] // Run the injector and capture FULL output
fn get_app_version(app: tauri::AppHandle) -> String { let output = Command::new(injector_path)
app.package_info().version.to_string() .arg("RocketLeague.exe")
} .arg(dll_path)
.output()
.map_err(|e| format!("Execution failed: {}", e))?;
#[tauri::command(rename_all = "snake_case")] let stdout = String::from_utf8_lossy(&output.stdout).to_string();
fn minimize_to_tray(window: WebviewWindow) { let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let _ = window.hide();
}
// --- main --- let full_log = format!("STDOUT:\n{}\n\nSTDERR:\n{}", stdout, stderr);
println!("[LOG] Injector results:\n{}", full_log);
if output.status.success() {
Ok(format!("Successfully injected!\n\n{}", stdout))
} else {
Err(format!("Injection failed!\n\n{}", full_log))
}
}
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.manage(AppState { .plugin(tauri_plugin_opener::init())
client: Client::builder() .plugin(tauri_plugin_process::init())
.danger_accept_invalid_certs(false) .plugin(tauri_plugin_updater::Builder::new().build())
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap(),
app_data: dirs::data_dir().expect("could not find data dir").join("RLidentity"),
sys: Mutex::new(System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::new())
)),
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
minimize_to_tray, minimize_to_tray,
save_config,
inject_dll, inject_dll,
validate_key, validate_key,
check_status, check_status,
get_hwid, get_hwid,
download_assets, download_assets
save_config,
get_app_version
]) ])
.setup(|app| { .setup(|app| {
let icon_bytes = include_bytes!("../icons/32x32.png");
let icon = tauri::image::Image::from_bytes(icon_bytes)?;
let window = app.get_webview_window("main").unwrap(); let window = app.get_webview_window("main").unwrap();
window.set_icon(icon.clone())?;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
apply_acrylic(&window, Some((18, 18, 18, 125))).ok(); apply_acrylic(&window, Some((18, 18, 18, 125))).ok();
let monitor_handle = app.handle().clone(); let handle = app.handle().clone();
tauri::async_runtime::spawn(async move { let tray_menu = tauri::menu::Menu::with_items(app, &[
let mut last_seen_running = false; &tauri::menu::MenuItem::with_id(app, "tray_quit", "Quit", true, None::<&str>)?,
loop { ])?;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let state = monitor_handle.state::<AppState>(); tauri::tray::TrayIconBuilder::new()
let is_running = { .icon(icon)
let mut sys = state.sys.lock().unwrap(); .menu(&tray_menu)
sys.refresh_processes_specifics(ProcessRefreshKind::new()); .on_menu_event(move |_app, event| {
let x = sys.processes_by_exact_name("RocketLeague.exe").next().is_some(); x if event.id().as_ref() == "tray_quit" { handle.exit(0); }
}; })
.on_tray_icon_event(|tray, event| {
if is_running && !last_seen_running { if let tauri::tray::TrayIconEvent::Click {
println!("auto-detect: rocket league started. injecting..."); button: tauri::tray::MouseButton::Left,
let _ = inject_dll(state).await; ..
} = event {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
} }
last_seen_running = is_running; })
} .build(app)?;
});
Ok(()) Ok(())
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "rlidentitygui", "productName": "rlidentitygui",
"version": "2.0.1", "version": "2.0.0",
"identifier": "me.rlidentity.gui", "identifier": "me.rlidentity.gui",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@ -19,29 +19,24 @@
"fullscreen": false, "fullscreen": false,
"transparent": false, "transparent": false,
"decorations": false, "decorations": false,
"devtools": false "devtools": true
} }
], ],
"security": { "security": {
"csp": "default-src 'self'; connect-src 'self' https://api.rlidentity.me https://git.rlidentity.me" "csp": null
} }
}, },
"plugins": { "plugins": {
"updater": { "updater": {
"endpoints": [ "endpoints": [
"https://api.rlidentity.me/version" "https://api.rlidentity.me/version"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFGNUM0MTdBMUQxQTBBQzUKUldURkNob2Rla0Zjcng4TWxpSWh3QVhlQ2xFZnRZUE5Ock1KQmk0T3ZkV25EQ1R2dWZrNWZPNUEK" "pubkey": "DWY+YmX5uY2E3N0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q0N3Q=="
} }
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"resources": [
"E:\\projects\\Rocket League\\RLIdentityDLL\\injector.exe",
"E:\\projects\\Rocket League\\RLIdentityDLL\\RLIdentity.dll"
],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",

View File

@ -1 +0,0 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFGNUM0MTdBMUQxQTBBQzUKUldURkNob2Rla0Zjcng4TWxpSWh3QVhlQ2xFZnRZUE5Ock1KQmk0T3ZkV25EQ1R2dWZrNWZPNUEK

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,22 @@
import { useMemo, useState, useEffect } from "react"; import { useMemo, useState, useEffect } from "react";
type Status = type Status =
| "ready" | "ready"
| "saved successfully" | "saved successfully"
| "injection started" | "injection started"
| "validating key..." | "validating key..."
| "invalid key" | "invalid key"
| "injecting..." | "injecting..."
| "RL Running" | "RL Running"
| "RL Closed" | "RL Closed"
| string; | string;
interface UserInfo { interface UserInfo {
user_id: string | null; userId: string | null;
discord_id: string | null; discordId: string | null;
epic_id: string | null; epicId: string | null;
username: string | null; username: string | null;
global_name: string | null; globalName: string | null;
logins?: number; logins?: number;
} }
@ -25,28 +25,16 @@ interface KeyValidationResponse {
user: UserInfo | null; user: UserInfo | null;
} }
const THEMES = [
{ id: "phantom", label: "Phantom", color: "#a855f7" },
{ id: "glacier", label: "Glacier", color: "#38bdf8" },
{ id: "inferno", label: "Inferno", color: "#f97316" },
{ id: "matrix", label: "Matrix", color: "#00ff41" },
{ id: "synthwave", label: "Synthwave", color: "#f72585" },
{ id: "eclipse", label: "Eclipse", color: "#fbbf24" },
] as const;
type ThemeId = typeof THEMES[number]["id"];
const LS_KEYS = { const LS_KEYS = {
spoofed: "rlidentity.spoofedUsername", spoofed: "neonGlass.spoofedUsername",
apiKey: "rlidentity.apiKey", apiKey: "neonGlass.apiKey",
minimizeToTray: "rlidentity.minimizeToTray", minimizeToTray: "neonGlass.minimizeToTray",
platform: "rlidentity.platform", platform: "neonGlass.platform",
autoInject: "rlidentity.autoInject", autoInject: "neonGlass.autoInject",
theme: "rlidentity.theme",
} as const; } as const;
const GITHUB_URL = "https://git.rlidentity.me/bits/rlidentity"; const GITHUB_URL = "https://github.com/RLidentity";
const FAQ_URL = "https://rlidentity.me/#faq"; const FAQ_URL = "https://rlidentity.me/faq";
function isTauriRuntime() { function isTauriRuntime() {
return typeof window !== "undefined" && typeof (window as any).__TAURI_INTERNALS__ !== "undefined"; return typeof window !== "undefined" && typeof (window as any).__TAURI_INTERNALS__ !== "undefined";
@ -68,6 +56,7 @@ async function tryInvoke<T>(cmd: string, args?: Record<string, unknown>) {
async function openUrl(url: string) { async function openUrl(url: string) {
const fallback = () => window.open(url, "_blank", "noopener,noreferrer"); const fallback = () => window.open(url, "_blank", "noopener,noreferrer");
if (!isTauriRuntime()) return fallback(); if (!isTauriRuntime()) return fallback();
try { try {
const mod: any = await import("@tauri-apps/plugin-opener"); const mod: any = await import("@tauri-apps/plugin-opener");
if (typeof mod.openUrl === "function") { if (typeof mod.openUrl === "function") {
@ -81,35 +70,30 @@ async function openUrl(url: string) {
} }
export default function App() { export default function App() {
const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []); const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []);
const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []); const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []);
const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []); const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []);
const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []); const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []);
const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []); const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []);
const initialTheme = useMemo(() => (localStorage.getItem(LS_KEYS.theme) ?? "phantom") as ThemeId, []);
const [version, setVersion] = useState(''); const [apiKey, setApiKey] = useState(initialApiKey);
const [apiKey, setApiKey] = useState(initialApiKey);
const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed); const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed);
const [isAuthorized, setIsAuthorized] = useState(false); const [isAuthorized, setIsAuthorized] = useState(false);
const [isRevoked, setIsRevoked] = useState(false); const [isRevoked, setIsRevoked] = useState(false);
const [userData, setUserData] = useState<UserInfo | null>(null); const [userData, setUserData] = useState<UserInfo | null>(null);
const [status, setStatus] = useState<Status>("ready"); const [status, setStatus] = useState<Status>("ready");
const [rlStatus, setRlStatus] = useState("Checking..."); const [rlStatus, setRlStatus] = useState("Checking...");
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false); const [logsOpen, setLogsOpen] = useState(false);
const [lastLog, setLastLog] = useState(""); const [lastLog, setLastLog] = useState("");
const [minimizeToTray, setMinimizeToTray] = useState(initialMinToTray); const [minimizeToTray, setMinimizeToTray] = useState(initialMinToTray);
const [platform, setPlatform] = useState(initialPlatform); const [platform, setPlatform] = useState(initialPlatform);
const [autoInject, setAutoInject] = useState(initialAutoInject); const [autoInject, setAutoInject] = useState(initialAutoInject);
const [platformPickerOpen, setPlatformPickerOpen] = useState(false); const [platformPickerOpen, setPlatformPickerOpen] = useState(false);
const [theme, setTheme] = useState<ThemeId>(initialTheme);
// Update modal // Easter Egg State
const [pendingUpdate, setPendingUpdate] = useState<{ version: string; install: () => Promise<void> } | null>(null); const [debugOpen, setDebugOpen] = useState(false);
// Easter egg
const [debugOpen, setDebugOpen] = useState(false);
const [logoClicks, setLogoClicks] = useState(0); const [logoClicks, setLogoClicks] = useState(0);
const handleLogoClick = (e: React.MouseEvent) => { const handleLogoClick = (e: React.MouseEvent) => {
@ -128,42 +112,31 @@ export default function App() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [logoClicks]); }, [logoClicks]);
// Apply theme // Tutorial State
useEffect(() => { const [tutorialStep, setTutorialStep] = useState(-1);
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem(LS_KEYS.theme, theme);
}, [theme]);
// Startup // Startup Authorization & Update Check
useEffect(() => { useEffect(() => {
if (initialApiKey) authorize(initialApiKey); if (initialApiKey) {
authorize(initialApiKey);
}
syncAssetsAndCheckUpdates(); syncAssetsAndCheckUpdates();
loadAppVersion();
}, []); }, []);
async function loadAppVersion() {
if (!isTauriRuntime()) return;
try {
const app = await import("@tauri-apps/api/app");
setVersion(await app.getVersion());
} catch (e) {
console.error("Failed to get app version:", e);
setVersion("");
}
}
async function syncAssetsAndCheckUpdates() { async function syncAssetsAndCheckUpdates() {
if (!isTauriRuntime()) return; if (!isTauriRuntime()) return;
try {
setStatus("syncing assets..."); // 1. Download DLL and Injector
await tryInvoke("download_assets"); try {
setStatus("ready"); await tryInvoke("download_assets");
} catch (e) { console.log("Assets synced successfully");
console.error("Failed to sync assets:", e); } catch (e) {
setStatus("Sync Error: " + String(e)); console.error("Failed to sync assets:", e);
}
// 2. Check for App updates
checkForUpdates();
} }
checkForUpdates();
}
async function checkForUpdates() { async function checkForUpdates() {
if (!isTauriRuntime()) return; if (!isTauriRuntime()) return;
@ -171,72 +144,89 @@ export default function App() {
const { check } = await import("@tauri-apps/plugin-updater"); const { check } = await import("@tauri-apps/plugin-updater");
const update = await check(); const update = await check();
if (update) { if (update) {
setPendingUpdate({ console.log(`Update available: ${update.version}`);
version: update.version, const confirmed = window.confirm(`A new version (${update.version}) is available. Would you like to update?`);
install: async () => { if (confirmed) {
setStatus("Updating..."); setStatus("Updating...");
await update.downloadAndInstall(); await update.downloadAndInstall();
const { relaunch } = await import("@tauri-apps/plugin-process"); // The app will restart automatically after install on some platforms,
await relaunch(); // or we might need to relaunch. Tauri v2 updater usually handles this.
}, const { relaunch } = await import("@tauri-apps/plugin-process");
}); await relaunch();
}
} }
} catch (e) { } catch (e) {
console.error("Failed to check for updates:", e); console.error("Failed to check for updates:", e);
} }
} }
// Revoked bg // Sync revoked background to body
useEffect(() => { useEffect(() => {
document.body.classList.toggle("revoked-bg", isRevoked); if (isRevoked) {
document.body.classList.add('revoked-bg');
} else {
document.body.classList.remove('revoked-bg');
}
}, [isRevoked]); }, [isRevoked]);
// Poll RL status + auto-inject // Poll for Rocket League status & Auto Inject
useEffect(() => { useEffect(() => {
if (!isAuthorized || isRevoked) return; if (!isAuthorized || isRevoked) return;
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const res = await tryInvoke<{ is_running: boolean }>("check_status"); const res = await tryInvoke<{is_running: boolean}>("check_status");
if (res) { if (res) {
const wasRunning = rlStatus === "RL Running"; const wasRunning = rlStatus === "RL Running";
const isRunning = res.is_running; const isRunning = res.is_running;
setRlStatus(isRunning ? "RL Running" : "RL Closed"); setRlStatus(isRunning ? "RL Running" : "RL Closed");
if (!wasRunning && isRunning && autoInject) inject();
// Auto Inject Logic: if it just started running and autoInject is on
if (!wasRunning && isRunning && autoInject) {
inject();
}
}
} catch (e) {
console.error(e);
} }
} catch (e) {
console.error(e);
}
}, 2000); }, 2000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [isAuthorized, isRevoked, rlStatus, autoInject]); }, [isAuthorized, isRevoked, rlStatus, autoInject]);
async function authorize(keyToTry: string) { async function authorize(keyToTry: string) {
if (!keyToTry.trim()) { setStatus("Please enter a key"); return; } if (!keyToTry.trim()) {
setStatus("Please enter a key");
return;
}
setStatus("validating key..."); setStatus("validating key...");
setIsRevoked(false); setIsRevoked(false);
try { try {
const hwid = await tryInvoke<string>("get_hwid") || "UNKNOWN-HWID"; const hwid = await tryInvoke<string>("get_hwid") || "UNKNOWN";
const res = await tryInvoke<KeyValidationResponse>("validate_key", { key: keyToTry.trim(), hwid }); const res = await tryInvoke<KeyValidationResponse>("validate_key", { key: keyToTry.trim(), hwid });
if (res?.status === "valid") { if (res && res.status === "valid") {
localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim()); localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim());
setUserData(res.user); setUserData(res.user);
setIsAuthorized(true); setIsAuthorized(true);
setIsRevoked(false); setIsRevoked(false);
setStatus("ready"); setStatus("ready");
} else if (res?.status === "revoked") {
setIsRevoked(true); // Check for tutorial
setIsAuthorized(false); if (res.user?.logins === 0) {
setStatus("Error: Key Revoked"); setTutorialStep(0);
} else if (res?.status === "invalid_hwid") { }
setStatus("Error: Key locked to another PC"); } else if (res && res.status === "revoked") {
setIsAuthorized(false); setIsRevoked(true);
} else { setIsAuthorized(false);
setStatus("Error: Invalid key"); setStatus("Error: Key Revoked");
setIsAuthorized(false); } else if (res && res.status === "invalid_hwid") {
} setStatus("Error: Key locked to another PC");
} catch { setIsAuthorized(false);
setStatus("Network Error: Check connection"); } else {
setStatus("Error: Invalid key");
setIsAuthorized(false);
}
} catch (e) {
setStatus("Network Error: Check connection");
} }
} }
@ -244,25 +234,25 @@ export default function App() {
localStorage.setItem(LS_KEYS.spoofed, spoofedUsername.trim()); localStorage.setItem(LS_KEYS.spoofed, spoofedUsername.trim());
localStorage.setItem(LS_KEYS.platform, platform); localStorage.setItem(LS_KEYS.platform, platform);
try { try {
await tryInvoke("save_config", { name: spoofedUsername.trim(), platform }); await tryInvoke("save_config", { name: spoofedUsername.trim(), platform });
setStatus("saved successfully"); setStatus("saved successfully");
window.setTimeout(() => setStatus("ready"), 1400); window.setTimeout(() => setStatus("ready"), 1400);
} catch (e) { } catch (e) {
setStatus("Save error: " + String(e)); setStatus("Save error: " + String(e));
} }
} }
async function inject() { async function inject() {
setStatus("injecting..."); setStatus("injecting...");
try { try {
const res = await tryInvoke<string>("inject_dll"); const res = await tryInvoke<string>("inject_dll", { discordId: userData?.discordId });
setLastLog(res || "Successfully Injected!"); setLastLog(res || "Successfully Injected!");
setStatus("Successfully Injected!"); setStatus("Successfully Injected!");
window.setTimeout(() => setStatus("ready"), 1400); window.setTimeout(() => setStatus("ready"), 1400);
} catch (e) { } catch (e) {
setStatus("Injection Failed!"); setStatus("Injection Failed!");
setLastLog(String(e)); setLastLog(String(e));
setLogsOpen(true); setLogsOpen(true);
} }
} }
@ -281,7 +271,7 @@ export default function App() {
if (minimizeToTray) { if (minimizeToTray) {
await tryInvoke("minimize_to_tray"); await tryInvoke("minimize_to_tray");
} else { } else {
await tryWindowApi(w => w.minimize()); await tryWindowApi((w) => w.minimize());
} }
} }
@ -290,53 +280,41 @@ export default function App() {
window.location.reload(); window.location.reload();
}; };
const closeAllModals = () => { const tutorialSteps = [
setSettingsOpen(false); { title: "Welcome to RLidentity", text: "Let's show you around. First, enter your spoofed username here.", target: "input" },
setLogsOpen(false); { title: "Injection", text: "Once Rocket League is running, click Inject to start. Or enable Auto-Inject in settings!", target: "btn-primary" },
setDebugOpen(false); { title: "Settings", text: "Customize your experience here. Change your platform or toggle Auto-Injection.", target: "tb-action" },
}; { title: "All Set!", text: "You're ready to win. Happy gaming!", target: "none" }
];
const isModalOpen = settingsOpen || logsOpen || debugOpen;
// ── Auth screen ───────────────────────────────────────────────────────────
if (!isAuthorized) { if (!isAuthorized) {
return ( return (
<div className="app-shell"> <div className="app-shell">
<div className={`bg-aurora ${isRevoked ? "revoked-aurora" : ""}`} aria-hidden="true" /> <div className={`bg-aurora ${isRevoked ? 'revoked-aurora' : ''}`} aria-hidden="true" />
<div className="window-titlebar" data-tauri-drag-region> <div className="window-titlebar" data-tauri-drag-region>
<div className="window-titlebar-left"> <div className="window-titlebar-left">
<img <img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
src="/rlidentity.webp"
className="app-logo"
alt="logo"
onClick={handleLogoClick}
draggable="false"
style={{ cursor: "default" }}
/>
<div className="titlebar-text" data-tauri-drag-region> <div className="titlebar-text" data-tauri-drag-region>
<div className="app-name neon-text-soft"> <div className="app-name neon-text-soft">RLidentity <span style={{ fontSize: '10px', opacity: 0.6, marginLeft: '4px' }}>v2.0.0</span></div>
RLidentity <span style={{ fontSize: "10px", opacity: 0.6, marginLeft: "4px" }}> <div className="app-slogan">{isRevoked ? 'License Revoked' : 'Authorize to continue'}</div>
v{version || "2.0.1"}
</span>
</div>
<div className="app-slogan">{isRevoked ? "License Revoked" : "Authorize to continue"}</div>
</div> </div>
</div> </div>
<div className="titlebar-controls"> <div className="titlebar-controls">
<button className="win-btn" onClick={handleMinimizeClick}></button> <button className="win-btn" onClick={handleMinimizeClick}></button>
<button className="win-btn" onClick={() => tryWindowApi(w => w.close())}></button> <button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}></button>
</div> </div>
</div> </div>
<main className="panel-wrap"> <main className="panel-wrap">
<section className={`glass-card neon-ring ${isRevoked ? "revoked" : ""}`} style={{ maxWidth: "400px", margin: "auto" }}> <section className={`glass-card neon-ring ${isRevoked ? 'revoked' : ''}`} style={{ maxWidth: '400px', margin: 'auto' }}>
<header className="card-header"> <header className="card-header">
<h1 className={`headline neon-text ${isRevoked ? "red" : ""}`}> <h1 className={`headline neon-text ${isRevoked ? 'red' : ''}`}>
{isRevoked ? "ACCESS REVOKED" : "RLidentity"} {isRevoked ? 'ACCESS REVOKED' : 'RLidentity'}
</h1> </h1>
<p className="app-slogan"> <p className="app-slogan">
{isRevoked ? "This license is no longer active" : "Enter your API key to continue"} {isRevoked ? 'This license is no longer active' : 'Enter your API key to continue'}
</p> </p>
</header> </header>
<div className="form-stack"> <div className="form-stack">
@ -348,24 +326,21 @@ export default function App() {
type="password" type="password"
className="input" className="input"
value={apiKey} value={apiKey}
onChange={e => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
onKeyDown={e => e.key === "Enter" && authorize(apiKey)}
placeholder="Enter your license key..." placeholder="Enter your license key..."
/> />
</div> </div>
</div> </div>
)} )}
<button className="btn btn-primary" onClick={() => isRevoked ? logout() : authorize(apiKey)}> <button className="btn btn-primary" onClick={() => isRevoked ? logout() : authorize(apiKey)}>
{isRevoked ? "Change Key" : "Login"} {isRevoked ? 'Change Key' : 'Login'}
</button> </button>
{status !== "ready" && ( {status !== "ready" && (
<p className="status-value" style={{ <p className="status-value" style={{textAlign:'center', marginTop:'10px', color: (isRevoked || status.startsWith('Error')) ? '#ff5555' : 'inherit'}}>
textAlign: "center", {status}
marginTop: "10px", </p>
color: (isRevoked || status.startsWith("Error")) ? "var(--red0)" : "inherit",
}}>
{status}
</p>
)} )}
</div> </div>
</section> </section>
@ -374,235 +349,234 @@ export default function App() {
); );
} }
// ── Main app ──────────────────────────────────────────────────────────────
return ( return (
<div className="app-shell"> <div className="app-shell">
<div className="bg-aurora" aria-hidden="true" /> <div className="bg-aurora" aria-hidden="true" />
<div className="window-titlebar" data-tauri-drag-region> <div className="window-titlebar" data-tauri-drag-region>
<div className="window-titlebar-left"> <div className="window-titlebar-left">
<img <img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
src="/rlidentity.webp" <div className="titlebar-app-name neon-text-soft" data-tauri-drag-region>RLidentity</div>
className="app-logo" </div>
alt="logo"
onClick={handleLogoClick} <div className="titlebar-controls-wrap" data-tauri-drag-region>
draggable="false" <div className="titlebar-controls">
style={{ cursor: "default" }} <button id="step-settings" className="tb-action" onClick={() => setSettingsOpen(true)}>Settings</button>
/> <button className="tb-action" onClick={() => openUrl(GITHUB_URL)}>GitHub</button>
<div className="titlebar-app-name neon-text-soft" data-tauri-drag-region>RLidentity</div> <button className="win-btn" onClick={handleMinimizeClick}></button>
</div> <button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}></button>
<div className="titlebar-controls-wrap" data-tauri-drag-region> </div>
<div className="titlebar-controls">
<button className="tb-action" onClick={() => setSettingsOpen(true)}>Settings</button>
<button className="tb-action" onClick={() => openUrl(GITHUB_URL)}>GitHub</button>
<button className="win-btn" onClick={handleMinimizeClick}></button>
<button className="win-btn" onClick={() => tryWindowApi(w => w.close())}></button>
</div> </div>
</div> </div>
</div>
<main className="panel-wrap"> <main className="panel-wrap">
<header className="welcome-section"> <header className="welcome-section">
<h2 className="welcome-text"> <h2 className="welcome-text">Welcome, <span className="neon-text-soft">{userData?.globalName || userData?.username || "User"}</span></h2>
Welcome, <span className="neon-text-soft">{userData?.global_name || userData?.username || "User"}</span> <div className="user-id-badge">User #{userData?.userId || "0"}</div>
</h2>
<div className="user-id-badge">User #{userData?.user_id || "0"}</div>
</header>
<section className="glass-card neon-ring">
<header className="card-header">
<h1 className="headline neon-text">Be anyone, Win everything</h1>
</header> </header>
<div className="form-stack"> <section className="glass-card neon-ring">
<div className="field"> <header className="card-header">
<label className="label">spoofed username</label> <h1 className="headline neon-text">Be anyone, Win everything</h1>
<div className="glass-input"> </header>
<input
className="input" <div className="form-stack">
value={spoofedUsername} <div className="field" id="step-username">
onChange={e => setSpoofedUsername(e.target.value)} <label className="label">spoofed username</label>
placeholder="Enter a username..." <div className="glass-input">
/> <input
className="input"
value={spoofedUsername}
onChange={(e) => setSpoofedUsername(e.target.value)}
/>
</div>
</div>
<button
id="step-inject"
className="btn btn-primary"
onClick={inject}
disabled={rlStatus !== "RL Running" && tutorialStep !== 1}
>
{rlStatus === "RL Running" ? "Inject" : "Start Rocket League"}
</button>
<div className="btn-row">
<button className="btn btn-secondary" onClick={() => openUrl(FAQ_URL)}>FAQ</button>
<button className="btn btn-tertiary" onClick={saveConfig}>Save Name</button>
</div> </div>
</div> </div>
<button <footer className="status-bar">
className="btn btn-primary" <div className="status-item">
onClick={inject} <span className="status-label">status</span>
disabled={rlStatus !== "RL Running"} <span className="status-value">{status}</span>
> </div>
{rlStatus === "RL Running" ? "Inject" : "Start Rocket League"} <div className="status-item">
</button> <span className="status-label">game</span>
<span className="status-value">{rlStatus}</span>
</div>
</footer>
</section>
</main>
<div className="btn-row"> {(settingsOpen || logsOpen || debugOpen) && (
<button className="btn btn-secondary" onClick={() => openUrl(FAQ_URL)}>FAQ</button> <div className="modal-overlay" onClick={() => { setSettingsOpen(false); setLogsOpen(false); setDebugOpen(false); }}>
<button className="btn btn-tertiary" onClick={saveConfig}>Save Name</button> <div className="modal glass-card neon-ring" onClick={e => e.stopPropagation()} style={{ maxWidth: (logsOpen || debugOpen) ? '600px' : '400px' }}>
{debugOpen ? (
<>
<h2 className="modal-title neon-text-soft">System Credits & Debug</h2>
<div className="glass-input" style={{ padding: '15px', marginBottom: '15px' }}>
<div style={{ display: 'grid', gap: '10px', fontSize: '13px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Lead Dev & Owner:</span>
<span className="neon-text-soft">Bits</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Dev & Admin:</span>
<span style={{ color: '#fff' }}>Danni</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Co-Owner:</span>
<span style={{ color: '#fff' }}>Deniz</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Administrator:</span>
<span style={{ color: '#fff' }}>Kairo</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Helpers:</span>
<span style={{ color: '#fff' }}>Quinn, SNDR</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Tester:</span>
<span style={{ color: '#fff' }}>Emir</span>
</div>
<hr style={{ border: 'none', borderTop: '1px solid rgba(255,255,255,0.1)', margin: '5px 0' }} />
<div style={{ textAlign: 'center' }}>
<button
className="btn btn-secondary"
style={{ width: '100%', marginTop: '5px' }}
onClick={() => openUrl('https://rlidentity.me/discord')}
>
Join Discord (rlidentity.me/discord)
</button>
</div>
</div>
</div>
<button className="btn btn-primary" onClick={() => setDebugOpen(false)}>Close Debug</button>
</>
) : logsOpen ? (
<>
<h2 className="modal-title neon-text-soft">Injection Logs</h2>
<div className="glass-input" style={{ height: '300px', padding: '10px', overflowY: 'auto' }}>
<pre style={{ fontSize: '12px', color: '#fff', whiteSpace: 'pre-wrap' }}>
{lastLog || "No logs yet..."}
</pre>
</div>
<button className="btn btn-primary" onClick={() => setLogsOpen(false)}>Close Logs</button>
</>
) : (
<>
<h2 className="modal-title neon-text-soft">Settings</h2>
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Platform</div>
</div>
<div className="custom-dropdown-wrap">
<button
className="glass-input dropdown-trigger"
onClick={() => setPlatformPickerOpen(!platformPickerOpen)}
>
<span className="dropdown-value">{platform === "Epic" ? "Epic Games" : "Steam"}</span>
<span className="dropdown-arrow"></span>
</button>
{platformPickerOpen && (
<div className="dropdown-menu glass-card neon-ring">
<button
className={`dropdown-item ${platform === "Epic" ? "active" : ""}`}
onClick={() => { setPlatform("Epic"); setPlatformPickerOpen(false); }}
>
Epic Games
</button>
<button
className={`dropdown-item ${platform === "Steam" ? "active" : ""}`}
onClick={() => { setPlatform("Steam"); setPlatformPickerOpen(false); }}
>
Steam
</button>
</div>
)}
</div>
</div>
<div className="setting-row" id="step-autoinject">
<div className="setting-text">
<div className="setting-title">Auto Injection</div>
<div className="setting-sub">Injects automatically when RL starts</div>
</div>
<label className="switch">
<input type="checkbox" checked={autoInject} onChange={e => toggleAutoInject(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Minimize to tray</div>
</div>
<label className="switch">
<input type="checkbox" checked={minimizeToTray} onChange={e => toggleMinimizeToTray(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
<div className="btn-row" style={{ flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
<button className="btn btn-tertiary" onClick={() => { setSettingsOpen(false); setLogsOpen(true); }}>View Last Injection Log</button>
<button className="btn btn-secondary" onClick={logout}>Logout</button>
<button className="btn btn-primary" onClick={() => setSettingsOpen(false)}>Close</button>
</div>
</>
)}
</div>
</div>
)}
{tutorialStep >= 0 && (
<div className="tutorial-overlay">
<div className={`tutorial-spotlight step-${tutorialStep}`} />
<div className={`tutorial-card glass-card neon-ring step-${tutorialStep}`}>
<h2 className="modal-title neon-text">{tutorialSteps[tutorialStep].title}</h2>
<p className="modal-p">{tutorialSteps[tutorialStep].text}</p>
<div className="btn-row">
<button
className="btn btn-secondary"
onClick={() => setTutorialStep(-1)}
>
Skip
</button>
<button
className="btn btn-primary"
onClick={() => {
if (tutorialStep === 2) {
setSettingsOpen(true);
}
if (tutorialStep < tutorialSteps.length - 1) {
setTutorialStep(tutorialStep + 1);
} else {
setSettingsOpen(false);
setTutorialStep(-1);
}
}}
>
{tutorialStep < tutorialSteps.length - 1 ? "Next" : "Finish"}
</button>
</div>
</div> </div>
</div> </div>
)}
<footer className="status-bar"> </div>
<div className="status-item">
<span className="status-label">status</span>
<span className="status-value">{status}</span>
</div>
<div className="status-item">
<span className="status-label">game</span>
<span className={`status-value ${rlStatus === "RL Running" ? "status-active" : ""}`}>{rlStatus}</span>
</div>
</footer>
</section>
</main>
{/* ── Modals ── */}
{isModalOpen && (
<div className="modal-overlay" onClick={closeAllModals}>
<div
className="modal glass-card neon-ring"
onClick={e => e.stopPropagation()}
style={{ maxWidth: logsOpen || debugOpen ? "600px" : "420px" }}
>
{debugOpen ? (
<>
<h2 className="modal-title neon-text-soft">System Credits</h2>
<div className="credits-grid glass-input">
{[
{ role: "Lead Dev & Owner", name: "Bits", accent: true },
{ role: "Dev & Admin", name: "Danni" },
{ role: "Co-Owner", name: "Deniz" },
{ role: "Administrator", name: "Kairo" },
{ role: "Helpers", name: "Quinn, SNDR" },
{ role: "Tester", name: "Emir" },
].map(({ role, name, accent }) => (
<div key={role} className="credit-row">
<span className="credit-role">{role}</span>
<span className={accent ? "neon-text-soft" : "credit-name"}>{name}</span>
</div>
))}
<hr className="credit-divider" />
<button className="btn btn-secondary" style={{ width: "100%" }} onClick={() => openUrl("https://rlidentity.me/discord")}>
Join Discord rlidentity.me/discord
</button>
</div>
<button className="btn btn-primary" style={{ marginTop: "14px" }} onClick={() => setDebugOpen(false)}>Close</button>
</>
) : logsOpen ? (
<>
<h2 className="modal-title neon-text-soft">Injection Logs</h2>
<div className="glass-input" style={{ height: "300px", padding: "10px", overflowY: "auto" }}>
<pre style={{ fontSize: "12px", color: "#fff", whiteSpace: "pre-wrap", margin: 0 }}>
{lastLog || "No logs yet..."}
</pre>
</div>
<button className="btn btn-primary" style={{ marginTop: "14px" }} onClick={() => setLogsOpen(false)}>Close Logs</button>
</>
) : (
<>
<h2 className="modal-title neon-text-soft">Settings</h2>
{/* Platform */}
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Platform</div>
</div>
<div className="custom-dropdown-wrap">
<button
className="dropdown-trigger"
onClick={() => setPlatformPickerOpen(p => !p)}
>
<span>{platform === "Epic" ? "Epic Games" : "Steam"}</span>
<span className="dropdown-arrow"></span>
</button>
{platformPickerOpen && (
<div className="dropdown-menu">
{["Epic", "Steam"].map(p => (
<button
key={p}
className={`dropdown-item ${platform === p ? "active" : ""}`}
onClick={() => { setPlatform(p); setPlatformPickerOpen(false); }}
>
{p === "Epic" ? "Epic Games" : "Steam"}
</button>
))}
</div>
)}
</div>
</div>
{/* Theme picker */}
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Theme</div>
<div className="setting-sub">Accent color</div>
</div>
<div className="theme-swatches">
{THEMES.map(t => (
<button
key={t.id}
className={`theme-swatch ${theme === t.id ? "active" : ""}`}
style={{ "--swatch-color": t.color } as React.CSSProperties}
onClick={() => setTheme(t.id)}
title={t.label}
aria-label={`${t.label} theme`}
/>
))}
</div>
</div>
{/* Auto inject */}
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Auto Injection</div>
<div className="setting-sub">Injects automatically when RL starts</div>
</div>
<label className="switch">
<input type="checkbox" checked={autoInject} onChange={e => toggleAutoInject(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
{/* Minimize to tray */}
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Minimize to tray</div>
</div>
<label className="switch">
<input type="checkbox" checked={minimizeToTray} onChange={e => toggleMinimizeToTray(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
<div className="btn-stack">
<button className="btn btn-tertiary" onClick={() => { setSettingsOpen(false); setLogsOpen(true); }}>
View Last Injection Log
</button>
<button className="btn btn-secondary" onClick={logout}>Logout</button>
<button className="btn btn-primary" onClick={() => setSettingsOpen(false)}>Close</button>
</div>
</>
)}
</div>
</div>
)}
{/* ── Update modal ── */}
{pendingUpdate && (
<div className="modal-overlay" onClick={() => setPendingUpdate(null)}>
<div className="modal glass-card neon-ring" onClick={e => e.stopPropagation()} style={{ maxWidth: "380px" }}>
<h2 className="modal-title neon-text-soft">Update Available</h2>
<p style={{ margin: "0 0 20px", color: "var(--muted)", fontSize: "14px" }}>
Version <strong style={{ color: "var(--text)" }}>{pendingUpdate.version}</strong> is ready to install.
The app will restart automatically.
</p>
<div className="btn-row">
<button className="btn btn-secondary" onClick={() => setPendingUpdate(null)}>Later</button>
<button className="btn btn-primary" style={{ width: "100%" }} onClick={pendingUpdate.install}>
Update Now
</button>
</div>
</div>
</div>
)}
</div>
); );
} }