-
+
-# OneClient | OneLauncher
-The monorepo containing the code for OneLauncher, OneClient, and their core backend.
-
-OneClient is a Minecraft client featuring fully 100% open-source components, offering many packaged and pre-configured mods in one click.
-OneLauncher is a WIP Minecraft launcher giving power-users the greatest customization whilst featuring a clean UI.
+forked from [OneLauncher](https://github.com/Polyfrost/OneLauncher) allowing for uh offline authentication
-## Installing
-
-You can install the latest release of OneClient from our website: [https://polyfrost.org/projects/oneclient](https://polyfrost.org/projects/oneclient)
-as well as our [GitHub releases](https://github.com/Polyfrost/OneLauncher/releases/latest).
-
-| Windows (x86_64) | macOS (Intel & Apple Silicon) | Linux (x86_64) |
-|------------------|-------------------------------|---------------------------------------------------------|
-| Installer 🔄 | DMG 🔄 | AppImage 🔄 |
-| | App Bundle 🔄 | DEB |
-| | | RPM |
-| | | [AUR](https://aur.archlinux.org/packages/oneclient-bin) |
-
-> 🔄 = Has support for autoupdating built-in
+## download from [**Releases Page**](https://github.com/oiupoyt/OneLauncher-Cracked/releases).
+yes the actual client performs normally aswell
-## Contributing
-We welcome contributions! Please read our [contributing guidelines](CONTRIBUTING.md) before getting started.
+intended for offline usage only.
-### Requirements
-
-The project targets **Rust 1.97** or later. You can install Rust via [rustup](https://rustup.rs/).
-
-
-### Building & Running
-
-```sh
-# Run the app
-cargo run -p oneclient_app
-
-# Build a release binary
-cargo build -p oneclient_app --release
-```
-
-
-### Packaging / Releasing
-
-Installers are produced with [**cargo-packager**](https://github.com/crabnebula-dev/cargo-packager)
-(the standalone bundler spun out of the Tauri bundler). Config lives in
-[`packages/oneclient_app/Cargo.toml`](./packages/oneclient_app/Cargo.toml) under
-`[package.metadata.packager]`.
-
-```sh
-cargo install cargo-packager --locked
-
-# Build the binary, then bundle it for the current OS:
-cargo build --release -p oneclient_app
-cargo packager --release -p oneclient_app --formats
-# Windows: nsis macOS: app,dmg Linux: deb,appimage
-```
-
-
-### Versioning
-
-The workspace shares a single version, defined in the root [`Cargo.toml`](./Cargo.toml) under `[workspace.package]`.
-
-
-## Code signing
-
-This program uses free code signing provided by [SignPath.io](https://signpath.io?utm_source=foundation&utm_medium=github&utm_campaign=0install), and a certificate by the [SignPath Foundation](https://signpath.org?utm_source=foundation&utm_medium=github&utm_campaign=0install). We thank them very much to their contributions to OSS software!
diff --git a/packages/oneclient_app/Cargo.toml b/packages/oneclient_app/Cargo.toml
index 6f7d3a0c..332d3406 100644
--- a/packages/oneclient_app/Cargo.toml
+++ b/packages/oneclient_app/Cargo.toml
@@ -45,6 +45,10 @@ regex.workspace = true
directories.workspace = true
dunce.workspace = true
percent-encoding.workspace = true
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+reqwest.workspace = true
+futures-util = { workspace = true, features = ["std"] }
cargo-packager-updater = "=0.2.3"
diff --git a/packages/oneclient_app/distribution/linux/oneclient_app.desktop b/packages/oneclient_app/distribution/linux/oneclient_app.desktop
new file mode 100644
index 00000000..f02aabbc
--- /dev/null
+++ b/packages/oneclient_app/distribution/linux/oneclient_app.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Name=OneClient
+Comment=Next-generation open source Minecraft launcher
+Exec=/usr/bin/oneclient_app
+Icon=oneclient_app
+Type=Application
+Terminal=false
+StartupNotify=true
+Categories=Game;ActionGame;AdventureGame;Simulation;
+Keywords=mc;minecraft;game;
+StartupWMClass=oneclient_app
diff --git a/packages/oneclient_app/src/components/account_switcher.rs b/packages/oneclient_app/src/components/account_switcher.rs
index 673b7dbf..11d9c827 100644
--- a/packages/oneclient_app/src/components/account_switcher.rs
+++ b/packages/oneclient_app/src/components/account_switcher.rs
@@ -6,14 +6,14 @@ use freya::{
use oneclient_auth::MinecraftAccount;
use crate::{
- ui::{divider},
Route,
components::{Avatar, Button, Icon, IconType, OverlayPopup},
hooks::{
- SetDefaultAccountKeys, try_accounts, try_default_account, use_account_switcher_open,
- use_accounts, use_current_account, use_dispatch, use_set_default_account,
+ try_accounts, try_default_account, use_account_switcher_open, use_accounts,
+ use_current_account, use_dispatch,
},
theme::colors,
+ ui::divider,
};
#[derive(PartialEq)]
@@ -90,8 +90,7 @@ impl Component for AccountPanel {
.spacing(8.)
.opacity(progress)
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
- .background(colors::page_elevated().with_a(220))
- .blur(12.)
+ .background(colors::page_elevated())
.corner_radius(CornerRadius::new_all(12.))
.border(
Border::new()
@@ -157,15 +156,19 @@ impl Component for AccountRow {
let active = self.active;
let dispatch = use_dispatch();
- let set_default = use_set_default_account();
let mut hovered = use_state(|| false);
let switch = move |_| {
if active {
return;
}
- set_default.mutate(SetDefaultAccountKeys { id: Some(id) });
dispatch.close_account_switcher();
+ spawn(async move {
+ if let Ok(state) = crate::launcher::state() {
+ let _ = state.auth.set_default_account(Some(id)).await;
+ crate::hooks::invalidate_auth_queries(Some(id)).await;
+ }
+ });
};
rect()
diff --git a/packages/oneclient_app/src/components/avatar.rs b/packages/oneclient_app/src/components/avatar.rs
index e1d5c488..1e62d72f 100644
--- a/packages/oneclient_app/src/components/avatar.rs
+++ b/packages/oneclient_app/src/components/avatar.rs
@@ -1,3 +1,6 @@
+use std::collections::HashMap;
+use std::sync::{Mutex, OnceLock};
+
use bytes::Bytes;
use freya::elements::image::{ImageHandle, image};
use freya::engine::prelude::{Paint, SkData, SkImage, raster_n32_premul};
@@ -10,6 +13,8 @@ const FACE: f32 = 8.;
const HEAD_FACE: (f32, f32) = (8., 8.);
const HEAD_OVERLAY: (f32, f32) = (40., 8.);
+static AVATAR_HEAD_CACHE: OnceLock>> = OnceLock::new();
+
#[derive(PartialEq, Clone)]
pub struct Avatar {
uuid: String,
@@ -34,22 +39,13 @@ impl LayoutExt for Avatar {
impl ContainerSizeExt for Avatar {}
impl Component for Avatar {
+ fn render_key(&self) -> DiffKey {
+ DiffKey::from(&self.uuid)
+ }
+
fn render(&self) -> impl IntoElement {
let (skin_bytes, _is_slim) = use_player_skin(self.uuid.clone());
-
- let mut cache = use_state(|| None::<(usize, ImageHandle)>);
- let src_ptr = skin_bytes.as_ptr() as usize;
- let cached = cache.read().clone();
- let head = match cached {
- Some((ptr, holder)) if ptr == src_ptr => Some(holder),
- _ => {
- let holder = compose_head(&skin_bytes);
- if let Some(holder) = &holder {
- cache.set(Some((src_ptr, holder.clone())));
- }
- holder
- }
- };
+ let head = get_or_compose_head(&skin_bytes);
rect()
.width(Size::px(AVATAR_SIZE))
@@ -67,6 +63,23 @@ impl Component for Avatar {
}
}
+fn get_or_compose_head(skin_bytes: &Bytes) -> Option {
+ if skin_bytes.is_empty() {
+ return None;
+ }
+ let src_ptr = skin_bytes.as_ptr() as usize;
+ let cache = AVATAR_HEAD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
+ if let Some(handle) = cache.lock().ok().and_then(|g| g.get(&src_ptr).cloned()) {
+ return Some(handle);
+ }
+
+ let composed = compose_head(skin_bytes)?;
+ if let Ok(mut guard) = cache.lock() {
+ guard.insert(src_ptr, composed.clone());
+ }
+ Some(composed)
+}
+
fn compose_head(skin_bytes: &Bytes) -> Option {
let data = unsafe { SkData::new_bytes(skin_bytes) };
let skin = SkImage::from_encoded(data)?;
diff --git a/packages/oneclient_app/src/components/microsoft_login.rs b/packages/oneclient_app/src/components/microsoft_login.rs
index aabe3044..47cb3bc7 100644
--- a/packages/oneclient_app/src/components/microsoft_login.rs
+++ b/packages/oneclient_app/src/components/microsoft_login.rs
@@ -1,9 +1,9 @@
+use crate::state::LoginProgress;
use freya::prelude::*;
use freya::query::{MutationCapability, MutationStateData, UseMutation};
use freya::text_edit::Clipboard;
-use oneclient_core::LauncherError;
use oneclient_auth::{AuthErrorGuidance, MicrosoftLoginSession};
-use crate::state::LoginProgress;
+use oneclient_core::LauncherError;
use crate::components::{Button, Icon, IconType, OverlayPopup};
use crate::hooks::{
@@ -185,7 +185,7 @@ pub(crate) fn login_dialog(
on_close: impl FnMut() + Clone + 'static,
) -> impl IntoElement {
let mut close_scrim = on_close.clone();
- let mut close_cancel = on_close;
+ let mut close_cancel = on_close.clone();
OverlayPopup::new()
.on_close(move |()| close_scrim())
.child(
@@ -225,10 +225,9 @@ pub(crate) fn login_dialog(
.spacing(18.)
.child(browser_dialog_body(auth_url))
.child(dialog_divider())
- .child(device_code_dialog_body(
- user_code,
- verification_uri,
- ))
+ .child(device_code_dialog_body(user_code, verification_uri))
+ .child(dialog_divider())
+ .child(offline_mode_dialog_body(on_close.clone()))
.child(status_row(status, error))
.maybe_child(guidance.map(guidance_block)),
),
@@ -314,17 +313,13 @@ fn status_row(status: Option, error: Option) -> impl Into
}
fn guidance_block(guidance: AuthErrorGuidance) -> impl IntoElement {
- let mut steps = rect()
- .vertical()
- .width(Size::fill())
- .spacing(6.)
- .child(
- label()
- .text("What you can do:")
- .font_size(12.)
- .font_weight(FontWeight::SEMI_BOLD)
- .color(colors::fg_primary()),
- );
+ let mut steps = rect().vertical().width(Size::fill()).spacing(6.).child(
+ label()
+ .text("What you can do:")
+ .font_size(12.)
+ .font_weight(FontWeight::SEMI_BOLD)
+ .color(colors::fg_primary()),
+ );
for (index, step) in guidance.steps_to_fix.into_iter().enumerate() {
steps = steps.child(
@@ -443,3 +438,39 @@ fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoE
)
.into_element()
}
+
+fn offline_mode_dialog_body(on_close: impl FnMut() + Clone + 'static) -> impl IntoElement {
+ let on_close_btn = on_close;
+
+ rect()
+ .vertical()
+ .width(Size::fill())
+ .cross_align(Alignment::Center)
+ .spacing(10.)
+ .child(
+ label()
+ .text("Or Play Offline (LAN / Offline Servers)")
+ .font_size(13.)
+ .font_weight(FontWeight::MEDIUM)
+ .color(colors::fg_secondary()),
+ )
+ .child(
+ Button::new()
+ .primary()
+ .on_press(move |_| {
+ let mut close_cb = on_close_btn.clone();
+ spawn(async move {
+ if let Ok(state) = crate::launcher::state() {
+ let res = state.auth.add_offline_account("Player".to_string()).await;
+ if let Ok(acc) = res {
+ let _ = state.auth.set_default_account(Some(acc.id)).await;
+ }
+ }
+ close_cb();
+ });
+ })
+ .text("Play Offline as Player")
+ .into_element(),
+ )
+ .into_element()
+}
diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs
index a49a39fa..d5dfcd04 100644
--- a/packages/oneclient_app/src/components/navbar/app_navbar.rs
+++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs
@@ -100,9 +100,9 @@ fn navbar_center() -> impl IntoElement {
nav_label: "Versions",
})
.child(NavLink {
- active: false,
- target: NavTarget::External("https://store.polyfrost.org"),
- nav_label: "Cosmetics",
+ active: route == Route::AccountSkins {},
+ target: NavTarget::Route(Route::AccountSkins {}),
+ nav_label: "Skins",
})
.child(NavLink {
active: matches!(
@@ -142,6 +142,7 @@ fn browse_target() -> Route {
}
#[derive(PartialEq, Clone)]
+#[allow(dead_code)]
enum NavTarget {
Route(Route),
External(&'static str),
diff --git a/packages/oneclient_app/src/components/player_model.rs b/packages/oneclient_app/src/components/player_model.rs
index 23524feb..46d178ea 100644
--- a/packages/oneclient_app/src/components/player_model.rs
+++ b/packages/oneclient_app/src/components/player_model.rs
@@ -78,6 +78,10 @@ impl PlayerModel {
}
impl Component for PlayerModel {
+ fn render_key(&self) -> DiffKey {
+ DiffKey::from(&self.uuid)
+ }
+
fn render(&self) -> impl IntoElement {
let (skin_bytes, is_slim) = use_player_skin(self.uuid.clone());
diff --git a/packages/oneclient_app/src/constants.rs b/packages/oneclient_app/src/constants.rs
index b4762806..6fa12a2a 100644
--- a/packages/oneclient_app/src/constants.rs
+++ b/packages/oneclient_app/src/constants.rs
@@ -1,6 +1,7 @@
pub const WINDOW_APP_ID: &str = "oneclient_app";
pub const WINDOW_TITLE: &str = "OneClient";
-pub const UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFGODk3MkMyMjg0MjFDMDUKUldRRkhFSW93bktKSHpkWjNEMXNzaDVINVpCTU8xSnhuK2RnV0dTZ2FkcFJWbG1zUkhGYTNjaUkK";
-pub const UPDATER_ENDPOINT: &str = "https://github.com/Polyfrost/OneLauncher/releases/latest/download/latest.json";
-pub const RELEASES_URL: &str = "https://github.com/Polyfrost/OneLauncher/releases/latest";
\ No newline at end of file
+pub const GITHUB_REPO: &str = "oiupoyt/OneLauncher-Cracked";
+pub const GITHUB_API_LATEST_RELEASE: &str =
+ "https://api.github.com/repos/oiupoyt/OneLauncher-Cracked/releases/latest";
+pub const RELEASES_URL: &str = "https://github.com/oiupoyt/OneLauncher-Cracked/releases/latest";
\ No newline at end of file
diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs
index 2a119519..f0c5e986 100644
--- a/packages/oneclient_app/src/hooks/mod.rs
+++ b/packages/oneclient_app/src/hooks/mod.rs
@@ -60,12 +60,15 @@ pub use queries::{
use_named_profiles, use_onboarding_bundles, use_package_categories, use_package_meta_batch,
use_package_project, use_package_search, use_package_versions, use_package_versions_when,
use_player_profile,
- use_player_skin, use_provider_versions, use_refresh_account, use_refresh_all_accounts,
+ use_player_skin, use_custom_skin, delete_account_skin, save_account_skin, invalidate_skin_queries,
+ use_provider_versions, use_refresh_account, use_refresh_all_accounts,
use_remove_account, use_screenshot_action, use_screenshot_folder_watch, use_set_default_account,
use_terms, use_upload_log,
use_version_metadata, use_versions, version_list, versions_metadata, versions_total,
};
+pub(crate) use queries::invalidate_auth_queries;
+
use crate::notifications::NotificationSnapshot;
use crate::state::{
AppChannel, GameState, InstallState, LauncherInit, LoginProgress, RelocationState,
diff --git a/packages/oneclient_app/src/hooks/queries/auth.rs b/packages/oneclient_app/src/hooks/queries/auth.rs
index fb5dccf6..527feba8 100644
--- a/packages/oneclient_app/src/hooks/queries/auth.rs
+++ b/packages/oneclient_app/src/hooks/queries/auth.rs
@@ -112,7 +112,7 @@ pub fn try_account(query: &UseQuery) -> Option {
super::state::settled_or_loading(query).flatten()
}
-async fn invalidate_auth_queries(account_id: Option) {
+pub(crate) async fn invalidate_auth_queries(account_id: Option) {
QueriesStorage::::invalidate_matching(ListAccountsKeys).await;
for fallback in [false, true] {
QueriesStorage::::invalidate_matching(DefaultAccountKeys { fallback })
diff --git a/packages/oneclient_app/src/hooks/queries/mod.rs b/packages/oneclient_app/src/hooks/queries/mod.rs
index d6efb2f9..e65694c6 100644
--- a/packages/oneclient_app/src/hooks/queries/mod.rs
+++ b/packages/oneclient_app/src/hooks/queries/mod.rs
@@ -95,8 +95,13 @@ pub use settings_profiles::{
try_game_profile, use_cluster_profile, use_cluster_settings, use_game_profile,
use_named_profiles,
};
-pub use skin::use_player_skin;
+pub use skin::{
+ delete_account_skin, invalidate_skin_queries, save_account_skin, use_custom_skin,
+ use_player_skin,
+};
pub use state::{query_error, query_is_busy, query_is_loading, settled_or_loading};
pub use tos::{TermsQuery, terms_document, terms_error, terms_is_loading, use_terms};
pub use version_metadata::{pick_version_metadata, use_version_metadata};
pub use versions::{loader_versions, use_loader_versions, use_versions, versions_metadata};
+
+pub(crate) use auth::invalidate_auth_queries;
diff --git a/packages/oneclient_app/src/hooks/queries/player_profile.rs b/packages/oneclient_app/src/hooks/queries/player_profile.rs
index 742b5ad7..58ef5892 100644
--- a/packages/oneclient_app/src/hooks/queries/player_profile.rs
+++ b/packages/oneclient_app/src/hooks/queries/player_profile.rs
@@ -22,12 +22,29 @@ impl QueryCapability for FetchPlayerProfileQuery {
type Keys = PlayerProfileQueryKeys;
async fn run(&self, keys: &Self::Keys) -> Result {
- let access_token = keys.access_token.clone();
+ let uuid_str = &keys.uuid;
+ if uuid_str.is_empty() {
+ return Ok(PlayerProfileView::default());
+ }
+ if let Ok(u) = uuid::Uuid::parse_str(uuid_str) {
+ let is_offline = u.get_version() == Some(uuid::Version::Md5) || u.is_nil();
+ if is_offline {
+ return Ok(PlayerProfileView {
+ uuid: uuid_str.clone(),
+ ..Default::default()
+ });
+ }
+ }
+
+ let access_token = keys.access_token.clone();
let state = crate::launcher::state()?;
let client = &state.services.requester;
- Ok(minecraft::fetch_player_profile_view(client, &keys.uuid, access_token.as_deref()).await?)
+ Ok(
+ minecraft::fetch_player_profile_view(client, &keys.uuid, access_token.as_deref())
+ .await?,
+ )
}
// TODO Cache
diff --git a/packages/oneclient_app/src/hooks/queries/skin.rs b/packages/oneclient_app/src/hooks/queries/skin.rs
index 10ab6609..0f4cd28a 100644
--- a/packages/oneclient_app/src/hooks/queries/skin.rs
+++ b/packages/oneclient_app/src/hooks/queries/skin.rs
@@ -1,9 +1,110 @@
use bytes::Bytes;
-use freya::prelude::*;
+use freya::query::{QueriesStorage, Query, QueryCapability, UseQuery, use_query};
+use oneclient_common::paths::{shared_minecraft_dir, skin_file_path, skin_meta_path, skins_dir};
+use serde::{Deserialize, Serialize};
+use std::time::Duration;
use crate::AppAssets;
+const SKIN_STALE: Duration = Duration::from_secs(5 * 60);
+const SKIN_CLEAN: Duration = Duration::from_secs(30 * 60);
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct CustomSkinKeys {
+ pub uuid: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CustomSkinData {
+ pub uuid: String,
+ pub has_custom: bool,
+ pub bytes: Option,
+ pub is_slim: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+struct SkinMetadata {
+ pub is_slim: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct CustomSkinQuery;
+
+impl QueryCapability for CustomSkinQuery {
+ type Ok = CustomSkinData;
+ type Err = String;
+ type Keys = CustomSkinKeys;
+
+ async fn run(&self, keys: &Self::Keys) -> Result {
+ let uuid = &keys.uuid;
+ if uuid.is_empty() {
+ return Ok(CustomSkinData {
+ uuid: String::new(),
+ has_custom: false,
+ bytes: None,
+ is_slim: false,
+ });
+ }
+
+ let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?;
+ let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?;
+
+ if !skin_path.exists() {
+ return Ok(CustomSkinData {
+ uuid: uuid.clone(),
+ has_custom: false,
+ bytes: None,
+ is_slim: false,
+ });
+ }
+
+ let bytes = polyio::read(&skin_path)
+ .await
+ .map_err(|e| format!("Failed to read custom skin: {e}"))?;
+
+ let is_slim = if meta_path.exists() {
+ polyio::read_to_string(&meta_path)
+ .await
+ .ok()
+ .and_then(|s| serde_json::from_str::(&s).ok())
+ .map(|m| m.is_slim)
+ .unwrap_or(false)
+ } else {
+ false
+ };
+
+ Ok(CustomSkinData {
+ uuid: uuid.clone(),
+ has_custom: true,
+ bytes: Some(Bytes::from(bytes)),
+ is_slim,
+ })
+ }
+}
+
+pub fn use_custom_skin(uuid: String) -> UseQuery {
+ use_query(
+ Query::new(CustomSkinKeys { uuid }, CustomSkinQuery)
+ .stale_time(SKIN_STALE)
+ .clean_time(SKIN_CLEAN),
+ )
+}
+
+pub async fn invalidate_skin_queries(uuid: Option<&str>) {
+ if let Some(uuid) = uuid {
+ QueriesStorage::::invalidate_matching(CustomSkinKeys {
+ uuid: uuid.to_string(),
+ })
+ .await;
+ } else {
+ QueriesStorage::::invalidate_all().await;
+ }
+}
+
pub fn use_player_skin(uuid: String) -> (Bytes, bool) {
+ let custom_query = use_custom_skin(uuid.clone());
+ let custom_data = crate::hooks::settled_or_loading(&custom_query);
+
let profile = super::use_player_profile(uuid.clone(), None::);
let (skin_url, is_slim) = crate::hooks::settled_or_loading(&profile)
@@ -11,22 +112,131 @@ pub fn use_player_skin(uuid: String) -> (Bytes, bool) {
let skin_query = super::use_cached_image(skin_url.clone(), 256);
- let steve = use_memo(|| AppAssets::get_bytes("steve.png").unwrap_or_default());
- let alex = use_memo(|| AppAssets::get_bytes("alex.png").unwrap_or_default());
+ let steve = AppAssets::get_bytes("steve.png").unwrap_or_default();
+ let alex = AppAssets::get_bytes("alex.png").unwrap_or_default();
let default_slim = (java_string_hash(&uuid) & 1) == 1;
+ if let Some((bytes, is_slim)) = custom_data
+ .filter(|c| c.has_custom)
+ .and_then(|c| c.bytes.map(|b| (b, c.is_slim)))
+ {
+ return (bytes, is_slim);
+ }
+
match crate::hooks::loaded_image(skin_url.as_deref(), &skin_query) {
Some((_, bytes)) => (bytes, is_slim),
- None if default_slim => (alex.read().clone(), true),
- None => (steve.read().clone(), false),
+ None if default_slim => (alex, true),
+ None => (steve, false),
}
}
-fn java_string_hash(s: &str) -> i32 {
+pub fn java_string_hash(s: &str) -> i32 {
let mut h: i32 = 0;
for c in s.encode_utf16() {
h = h.wrapping_mul(31).wrapping_add(c as i32);
}
h
}
+
+pub async fn save_account_skin(
+ uuid: &str,
+ username: Option<&str>,
+ skin_bytes: &[u8],
+ is_slim: bool,
+) -> Result<(), String> {
+ let skins_folder = skins_dir().map_err(|e| e.to_string())?;
+ polyio::create_dir_all(&skins_folder)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?;
+ let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?;
+
+ let img = image::load_from_memory(skin_bytes)
+ .map_err(|e| format!("Invalid image format: {e}"))?
+ .to_rgba8();
+
+ let (w, h) = (img.width(), img.height());
+
+ let normalized_img = if w == 64 && h == 32 {
+ let mut new_img = image::RgbaImage::new(64, 64);
+ image::imageops::overlay(&mut new_img, &img, 0, 0);
+ new_img
+ } else if w == 64 && h == 64 {
+ img
+ } else {
+ return Err(format!(
+ "Invalid Minecraft skin dimensions: {w}x{h}. Must be 64x64 or 64x32."
+ ));
+ };
+
+ let mut png_bytes = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut png_bytes);
+ normalized_img
+ .write_to(&mut cursor, image::ImageFormat::Png)
+ .map_err(|e| format!("Failed to encode PNG: {e}"))?;
+
+ polyio::write(&skin_path, &png_bytes)
+ .await
+ .map_err(|e| format!("Failed to write skin file: {e}"))?;
+
+ let meta = SkinMetadata { is_slim };
+ let meta_json = serde_json::to_string_pretty(&meta)
+ .map_err(|e| format!("Failed to serialize metadata: {e}"))?;
+
+ polyio::write(&meta_path, meta_json.as_bytes())
+ .await
+ .map_err(|e| format!("Failed to write skin metadata: {e}"))?;
+
+ if let Some(uname) = username.filter(|u| !u.is_empty()) {
+ let _ = sync_custom_skin_loader(uname, &png_bytes).await;
+ }
+
+ invalidate_skin_queries(Some(uuid)).await;
+
+ Ok(())
+}
+
+pub async fn delete_account_skin(uuid: &str, username: Option<&str>) -> Result<(), String> {
+ let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?;
+ let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?;
+
+ if skin_path.exists() {
+ let _ = polyio::remove_file(&skin_path).await;
+ }
+ if meta_path.exists() {
+ let _ = polyio::remove_file(&meta_path).await;
+ }
+
+ if let Some(uname) = username.filter(|u| !u.is_empty()) {
+ let _ = remove_custom_skin_loader(uname).await;
+ }
+
+ invalidate_skin_queries(Some(uuid)).await;
+
+ Ok(())
+}
+
+async fn sync_custom_skin_loader(username: &str, skin_png: &[u8]) -> Result<(), String> {
+ if let Ok(mc_dir) = shared_minecraft_dir() {
+ let csl_skins_dir = mc_dir.join("CustomSkinLoader").join("skins");
+ let _ = polyio::create_dir_all(&csl_skins_dir).await;
+ let target = csl_skins_dir.join(format!("{username}.png"));
+ let _ = polyio::write(&target, skin_png).await;
+ }
+ Ok(())
+}
+
+async fn remove_custom_skin_loader(username: &str) -> Result<(), String> {
+ if let Ok(mc_dir) = shared_minecraft_dir() {
+ let target = mc_dir
+ .join("CustomSkinLoader")
+ .join("skins")
+ .join(format!("{username}.png"));
+ if target.exists() {
+ let _ = polyio::remove_file(&target).await;
+ }
+ }
+ Ok(())
+}
diff --git a/packages/oneclient_app/src/layout/root_layout.rs b/packages/oneclient_app/src/layout/root_layout.rs
index 92bc474c..b891bad3 100644
--- a/packages/oneclient_app/src/layout/root_layout.rs
+++ b/packages/oneclient_app/src/layout/root_layout.rs
@@ -47,10 +47,10 @@ impl Component for RootLayout {
ctx.windows.get(&id).map(|w| w.window().is_maximized())
})
.await;
- if let Ok(Some(is_max)) = is_max {
- if *maximized.peek() != is_max {
- maximized.set(is_max);
- }
+ if let Ok(Some(is_max)) = is_max
+ && *maximized.peek() != is_max
+ {
+ maximized.set(is_max);
}
});
});
diff --git a/packages/oneclient_app/src/updater.rs b/packages/oneclient_app/src/updater.rs
index f46d288a..1e16a71a 100644
--- a/packages/oneclient_app/src/updater.rs
+++ b/packages/oneclient_app/src/updater.rs
@@ -1,10 +1,9 @@
-use std::cell::Cell;
+use std::path::PathBuf;
-use cargo_packager_updater::{Config, Update, check_update};
use oneclient_events::{Choice, EventBus, Prompt};
use uuid::Uuid;
-use crate::constants::{RELEASES_URL, UPDATER_ENDPOINT, UPDATER_PUBKEY};
+use crate::constants::GITHUB_API_LATEST_RELEASE;
pub const UPDATE_CHOICE_INSTALL: &str = "update.install";
@@ -15,7 +14,7 @@ enum UpdateAnswer {
fn update_prompt(version: &str) -> Prompt {
Prompt::new(
"Update available",
- format!("OneClient {version} is ready to install. Download and install it now?"),
+ format!("OneLauncher-Cracked {version} is ready to install. Download and install it now?"),
)
.option(
Choice::primary(UPDATE_CHOICE_INSTALL, "Install"),
@@ -26,6 +25,95 @@ fn update_prompt(version: &str) -> Prompt {
const PROGRESS_STEP: u64 = 256 * 1024;
+/// SemVer parser supporting cracked revision suffixes like `2.2.3-c1`, `2.2.3.c1`, `v2.2.3-c2`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct CrackedVersion {
+ pub major: u64,
+ pub minor: u64,
+ pub patch: u64,
+ pub rev: u64,
+}
+
+impl CrackedVersion {
+ pub fn parse(s: &str) -> Option {
+ let trimmed = s.trim().trim_start_matches(['v', 'V']);
+
+ let (base, rev) = if let Some((base_part, rev_part)) = trimmed.split_once("-c").or_else(|| trimmed.split_once(".c")) {
+ let rev_num = rev_part.parse::().ok()?;
+ (base_part, rev_num)
+ } else if let Some((base_part, rev_part)) = trimmed.split_once("-r").or_else(|| trimmed.split_once(".r")) {
+ let rev_num = rev_part.parse::().ok()?;
+ (base_part, rev_num)
+ } else if let Some((base_part, rev_part)) = trimmed.split_once('-') {
+ let rev_num = rev_part.trim_start_matches(['c', 'C', 'r', 'R']).parse::().unwrap_or(0);
+ (base_part, rev_num)
+ } else {
+ (trimmed, 0)
+ };
+
+ let mut parts = base.split('.');
+ let major = parts.next()?.parse::().ok()?;
+ let minor = parts.next()?.parse::().ok()?;
+ let patch = parts.next().unwrap_or("0").parse::().ok()?;
+
+ Some(Self {
+ major,
+ minor,
+ patch,
+ rev,
+ })
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct GithubRelease {
+ pub tag_name: String,
+ pub name: Option,
+ pub body: Option,
+ pub html_url: String,
+ #[serde(default)]
+ pub assets: Vec,
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct GithubReleaseAsset {
+ pub name: String,
+ pub size: u64,
+ pub browser_download_url: String,
+}
+
+impl GithubRelease {
+ pub fn find_installer_asset(&self) -> Option<&GithubReleaseAsset> {
+ #[cfg(target_os = "windows")]
+ {
+ self.assets.iter().find(|a| a.name.ends_with("_x64-setup.exe") || a.name.ends_with("-setup.exe") || a.name.ends_with(".exe"))
+ }
+ #[cfg(target_os = "linux")]
+ {
+ if std::env::var_os("APPIMAGE").is_some() {
+ self.assets.iter().find(|a| a.name.ends_with(".AppImage"))
+ } else {
+ self.assets.iter().find(|a| a.name.ends_with(".deb") || a.name.ends_with(".AppImage") || a.name.ends_with(".rpm"))
+ }
+ }
+ #[cfg(target_os = "macos")]
+ {
+ #[cfg(target_arch = "aarch64")]
+ {
+ self.assets.iter().find(|a| a.name.ends_with("aarch64.dmg") || a.name.ends_with("arm64.dmg") || a.name.ends_with(".dmg"))
+ }
+ #[cfg(not(target_arch = "aarch64"))]
+ {
+ self.assets.iter().find(|a| a.name.ends_with("x64.dmg") || a.name.ends_with("x86_64.dmg") || a.name.ends_with(".dmg"))
+ }
+ }
+ #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
+ {
+ None
+ }
+ }
+}
+
pub fn spawn_update_check(auto_install: bool, events: EventBus) {
tokio::spawn(async move {
if let Err(err) = run_check(auto_install, events).await {
@@ -44,7 +132,7 @@ pub fn spawn_simulated_update() {
}
async fn run_simulated_update() -> anyhow::Result<()> {
- const FAKE_VERSION: &str = "9999.9999.9999";
+ const FAKE_VERSION: &str = "2.2.3-c99";
const FAKE_TOTAL: u64 = 48 * 1024 * 1024;
let events = crate::launcher::state()?.services.events.clone();
@@ -55,7 +143,7 @@ async fn run_simulated_update() -> anyhow::Result<()> {
}
let progress_id = Uuid::new_v4();
- let label = format!("Downloading OneClient {FAKE_VERSION}");
+ let label = format!("Downloading OneLauncher-Cracked {FAKE_VERSION}");
let mut downloaded = 0u64;
events.progress(progress_id, &label, downloaded, FAKE_TOTAL);
@@ -68,116 +156,230 @@ async fn run_simulated_update() -> anyhow::Result<()> {
events.finish_progress(
progress_id,
"Finished Downloading",
- format!("OneClient {FAKE_VERSION} is ready. Restart to apply."),
+ format!("OneLauncher-Cracked {FAKE_VERSION} is ready. Restart to apply."),
);
Ok(())
}
async fn run_check(auto_install: bool, events: EventBus) -> anyhow::Result<()> {
- // `check_update` performs a blocking HTTP request so offload it to a thread pool
- let Some(update) = tokio::task::spawn_blocking(check_for_update).await?? else {
+ let Some(release) = check_for_github_update().await? else {
tracing::info!("no update available");
return Ok(());
};
- tracing::info!("update available: {}", update.version);
+ tracing::info!("update available: {}", release.tag_name);
- // cargo-packager-updater can only replace an AppImage in place deb/rpm installs live
- // under a package-managed path that an install would fail on or clobber
if !can_self_update() {
- tracing::info!("install is not self-updatable (non-AppImage Linux); notifying only");
+ tracing::info!("install is not self-updatable in-place; notifying with release URL");
events
.notify("Update available")
.body(format!(
- "OneClient {} is available. Download the latest package from {} to update.",
- update.version, RELEASES_URL
+ "OneLauncher-Cracked {} is available. Download from {}",
+ release.tag_name, release.html_url
))
.send();
return Ok(());
}
- if !auto_install && events.ask(update_prompt(&update.version)).await?.is_none() {
- tracing::info!("user declined update {}", update.version);
+ if !auto_install && events.ask(update_prompt(&release.tag_name)).await?.is_none() {
+ tracing::info!("user declined update {}", release.tag_name);
return Ok(());
}
- download_and_install(update, events).await
+ download_and_install_github(release, events).await
}
fn can_self_update() -> bool {
- if cfg!(debug_assertions) {
- return false;
- }
+ if cfg!(debug_assertions) {
+ return false;
+ }
- if std::env::var_os("ONECLIENT_DISABLE_AUTOUPDATE").is_some_and(|val| val.eq_ignore_ascii_case("1")) {
- return false;
- }
+ if std::env::var_os("ONECLIENT_DISABLE_AUTOUPDATE")
+ .is_some_and(|val| val.eq_ignore_ascii_case("1"))
+ {
+ return false;
+ }
+ #[cfg(target_os = "windows")]
+ {
+ true
+ }
#[cfg(target_os = "linux")]
{
std::env::var_os("APPIMAGE").is_some()
}
- #[cfg(not(target_os = "linux"))]
+ #[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
- true
+ false
}
}
-fn check_for_update() -> anyhow::Result