feat: update base to official upstream 2.5.1 with all cracked features intact (2.5.1-c1)
Some checks are pending
Release Multiplatform Binaries / build (deb,appimage, linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-arm64, macos-14, aarch64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-x86_64, macos-14, x86_64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (nsis, windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Release Multiplatform Binaries / create-release (push) Blocked by required conditions
BIN
.github/media/RepoBanner.png
vendored
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 21 KiB |
8
.github/workflows/release.yml
vendored
|
|
@ -7,9 +7,9 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release Tag (e.g. v2.5.0)'
|
description: 'Release Tag (e.g. v2.5.1)'
|
||||||
required: false
|
required: false
|
||||||
default: 'v2.5.0-c1'
|
default: 'v2.5.1-c1'
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -138,7 +138,7 @@ jobs:
|
||||||
elif [ -n "${{ inputs.tag }}" ]; then
|
elif [ -n "${{ inputs.tag }}" ]; then
|
||||||
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "tag=v2.5.0-c1" >> $GITHUB_OUTPUT
|
echo "tag=v2.5.1-c1" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Clean up previous release if exists
|
- name: Clean up previous release if exists
|
||||||
|
|
@ -155,7 +155,7 @@ jobs:
|
||||||
name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}"
|
name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}"
|
||||||
body: |
|
body: |
|
||||||
### What's New in ${{ steps.release_tag.outputs.tag }}
|
### What's New in ${{ steps.release_tag.outputs.tag }}
|
||||||
- **Rebased onto Official Upstream 2.5.0**: Includes all upstream 2.5.0 updates (New Control Center quick settings & system info, refined navbar styling, bundle version-tag matching and compatibility filtering, version metadata self-healing).
|
- **Rebased onto Official Upstream 2.5.1**: Includes all upstream 2.5.1 updates (Process hook error handling and execution reporting, optimized bundled packages sync after launch, auto-refresh of stale package metadata, asset optimizations).
|
||||||
- **Guaranteed Offline Game Launch**: Completely eliminated the upstream check in `account_for_launch` that blocked offline accounts from launching without Microsoft accounts. Offline accounts can launch directly with 0 Microsoft accounts required.
|
- **Guaranteed Offline Game Launch**: Completely eliminated the upstream check in `account_for_launch` that blocked offline accounts from launching without Microsoft accounts. Offline accounts can launch directly with 0 Microsoft accounts required.
|
||||||
- **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, slim/classic model toggle, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation.
|
- **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, slim/classic model toggle, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation.
|
||||||
- **Offline / Cracked Accounts**: Full offline account support in onboarding and settings with inline username editing.
|
- **Offline / Cracked Accounts**: Full offline account support in onboarding and settings with inline username editing.
|
||||||
|
|
|
||||||
30
Cargo.lock
generated
|
|
@ -4480,7 +4480,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_app"
|
name = "oneclient_app"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|
@ -4530,7 +4530,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_auth"
|
name = "oneclient_auth"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -4554,7 +4554,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_cluster"
|
name = "oneclient_cluster"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.11.1",
|
"bytes 1.11.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -4577,7 +4577,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_common"
|
name = "oneclient_common"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
"interfrost",
|
"interfrost",
|
||||||
|
|
@ -4590,7 +4590,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_content"
|
name = "oneclient_content"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
|
|
@ -4618,7 +4618,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_core"
|
name = "oneclient_core"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|
@ -4683,7 +4683,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_db"
|
name = "oneclient_db"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-tempfile",
|
"async-tempfile",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -4698,7 +4698,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_discord"
|
name = "oneclient_discord"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
|
|
@ -4710,7 +4710,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_events"
|
name = "oneclient_events"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -4720,7 +4720,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_java"
|
name = "oneclient_java"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
"async-tempfile",
|
"async-tempfile",
|
||||||
|
|
@ -4743,7 +4743,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_macro"
|
name = "oneclient_macro"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -4752,7 +4752,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_mc"
|
name = "oneclient_mc"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -4774,7 +4774,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_net"
|
name = "oneclient_net"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|
@ -4796,7 +4796,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneclient_polyplus"
|
name = "oneclient_polyplus"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"bytes 1.11.1",
|
"bytes 1.11.1",
|
||||||
|
|
@ -5094,7 +5094,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polyio"
|
name = "polyio"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ license = "GPL-3.0-only"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/oiupoyt/OneLauncher-Cracked"
|
repository = "https://github.com/oiupoyt/OneLauncher-Cracked"
|
||||||
rust-version = "1.97"
|
rust-version = "1.97"
|
||||||
version = "2.5.0-c1"
|
version = "2.5.1-c1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# core
|
# core
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ makedepends=(
|
||||||
)
|
)
|
||||||
provides=('oneclient')
|
provides=('oneclient')
|
||||||
conflicts=('oneclient-bin')
|
conflicts=('oneclient-bin')
|
||||||
options=('!lto', '!debug')
|
options=(
|
||||||
|
'!lto'
|
||||||
|
'!debug'
|
||||||
|
)
|
||||||
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 920 B |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 840 B |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -1373,13 +1373,9 @@ impl Actions {
|
||||||
|
|
||||||
spawn_forever(async move {
|
spawn_forever(async move {
|
||||||
|
|
||||||
let synced = match state.bundles.sync(&state.services.content()).await {
|
if let Err(err) = state.bundles.sync(&state.services.content()).await {
|
||||||
Ok(_) => true,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("bundle catalog sync failed: {err:#}");
|
tracing::error!("bundle catalog sync failed: {err:#}");
|
||||||
false
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await {
|
if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await {
|
||||||
tracing::error!("cluster migrations failed: {err:#}");
|
tracing::error!("cluster migrations failed: {err:#}");
|
||||||
}
|
}
|
||||||
|
|
@ -1387,37 +1383,6 @@ impl Actions {
|
||||||
tracing::error!("bundle cluster provisioning failed: {err:#}");
|
tracing::error!("bundle cluster provisioning failed: {err:#}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// One grouped session for the batch so all downloads surface as a
|
|
||||||
// single notification `detach` lets it be converted in place
|
|
||||||
let session = oneclient_events::GroupedProgressSession::start(
|
|
||||||
&state.services.events,
|
|
||||||
"Updating mods",
|
|
||||||
);
|
|
||||||
let changed = oneclient_content::bundles::sync_all_cluster_bundles(
|
|
||||||
state.bundles.as_ref(),
|
|
||||||
&state.services.content(),
|
|
||||||
Some(&session),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let session_id = session.detach();
|
|
||||||
|
|
||||||
if synced
|
|
||||||
&& let Ok(clusters) = state.clusters.list().await
|
|
||||||
{
|
|
||||||
let unsettled: std::collections::HashSet<i64> = changed
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, result)| !result.settled())
|
|
||||||
.map(|(cluster_id, _)| *cluster_id)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
actions.record_bundle_checks(
|
|
||||||
clusters
|
|
||||||
.iter()
|
|
||||||
.map(|cluster| cluster.id)
|
|
||||||
.filter(|cluster_id| !unsettled.contains(cluster_id)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
actions
|
actions
|
||||||
.station
|
.station
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -1425,12 +1390,6 @@ impl Actions {
|
||||||
.launcher
|
.launcher
|
||||||
.syncing_bundles = false;
|
.syncing_bundles = false;
|
||||||
super::invalidate_cluster_queries().await;
|
super::invalidate_cluster_queries().await;
|
||||||
|
|
||||||
let spec = crate::install::combined_cluster_update_spec(&changed, &state.services).await;
|
|
||||||
actions.with_engine(|app| {
|
|
||||||
app.notifications
|
|
||||||
.finish_grouped_as_actions(&mut app.inbox, session_id, spec);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1451,8 +1410,6 @@ impl Actions {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mode = self.cluster_update_mode(state, cluster_id).await;
|
|
||||||
|
|
||||||
self.station
|
self.station
|
||||||
.clone()
|
.clone()
|
||||||
.write_channel(AppChannel::Launcher)
|
.write_channel(AppChannel::Launcher)
|
||||||
|
|
@ -1486,15 +1443,8 @@ impl Actions {
|
||||||
.launcher
|
.launcher
|
||||||
.syncing_bundles = false;
|
.syncing_bundles = false;
|
||||||
|
|
||||||
// No prompt for bundles the package manager's markers carry them instead
|
// Bundle content is forced, so the update mode never gates it and the
|
||||||
if !matches!(mode, PackageUpdateMode::Automatic) {
|
// modal never lists it
|
||||||
tracing::debug!(cluster_id, "bundle updates left for the user to apply");
|
|
||||||
if synced {
|
|
||||||
self.record_bundle_checks([cluster_id]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match oneclient_core::apply_bundle_updates(
|
let result = match oneclient_core::apply_bundle_updates(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
state.bundles.as_ref(),
|
state.bundles.as_ref(),
|
||||||
|
|
@ -1533,12 +1483,6 @@ impl Actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
super::invalidate_cluster_queries().await;
|
super::invalidate_cluster_queries().await;
|
||||||
|
|
||||||
if let Some(spec) =
|
|
||||||
crate::install::cluster_update_notification(cluster_id, &result, &state.services).await
|
|
||||||
{
|
|
||||||
self.push_notification(spec);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The check runs and the cache is written whatever the mode says keeping
|
/// The check runs and the cache is written whatever the mode says keeping
|
||||||
|
|
|
||||||
|
|
@ -168,60 +168,6 @@ pub async fn cluster_update_notification(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All clusters share one "View changes" action so the notification keeps exactly two buttons
|
|
||||||
pub async fn combined_cluster_update_spec(
|
|
||||||
changed: &[(i64, oneclient_core::ApplyBundleUpdatesResult)],
|
|
||||||
services: &oneclient_core::LauncherServices,
|
|
||||||
) -> Option<NotificationSpec> {
|
|
||||||
let mut summaries = Vec::new();
|
|
||||||
let mut total_changes = 0usize;
|
|
||||||
|
|
||||||
for (cluster_id, result) in changed {
|
|
||||||
if let Some(summary) = cluster_update_summary(*cluster_id, result, services).await {
|
|
||||||
total_changes += summary.total();
|
|
||||||
summaries.push(summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if summaries.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cluster_count = summaries.len();
|
|
||||||
let (title, body) = if total_changes == 0 {
|
|
||||||
let offers: usize = summaries.iter().map(|s| s.optional.len()).sum();
|
|
||||||
(
|
|
||||||
"Optional mods available",
|
|
||||||
format!(
|
|
||||||
"{offers} optional mod{} across {cluster_count} cluster{}",
|
|
||||||
if offers == 1 { "" } else { "s" },
|
|
||||||
if cluster_count == 1 { "" } else { "s" }
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
"Mods updated",
|
|
||||||
format!(
|
|
||||||
"{total_changes} package{} updated across {cluster_count} cluster{}",
|
|
||||||
if total_changes == 1 { "" } else { "s" },
|
|
||||||
if cluster_count == 1 { "" } else { "s" }
|
|
||||||
),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(NotificationSpec {
|
|
||||||
title: title.to_string(),
|
|
||||||
body,
|
|
||||||
level: Level::Info,
|
|
||||||
icon: Some(IconType::DownloadCloud02),
|
|
||||||
progress: None,
|
|
||||||
actions: vec![NotificationAction {
|
|
||||||
label: "View changes".to_string(),
|
|
||||||
kind: NotificationActionKind::OpenClusterUpdate(summaries),
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn pending_optional_group(
|
pub async fn pending_optional_group(
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
pending: &[oneclient_core::PendingOptionalMod],
|
pending: &[oneclient_core::PendingOptionalMod],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ mod manifest;
|
||||||
mod optional;
|
mod optional;
|
||||||
mod overrides;
|
mod overrides;
|
||||||
mod polymrpack;
|
mod polymrpack;
|
||||||
mod runtime;
|
|
||||||
mod types;
|
mod types;
|
||||||
mod updates;
|
mod updates;
|
||||||
|
|
||||||
|
|
@ -25,7 +24,6 @@ pub use optional::{
|
||||||
PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods,
|
PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods,
|
||||||
};
|
};
|
||||||
pub use manifest::BundleManifest as RemoteBundleManifest;
|
pub use manifest::BundleManifest as RemoteBundleManifest;
|
||||||
pub use runtime::{is_bundle_syncing, sync_all_cluster_bundles};
|
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleOptionalPackage,
|
ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleOptionalPackage,
|
||||||
BundleManifest, BundlePackageAddition, BundlePackageRemoval, BundlePackageUpdate,
|
BundleManifest, BundlePackageAddition, BundlePackageRemoval, BundlePackageUpdate,
|
||||||
|
|
|
||||||
|
|
@ -902,34 +902,6 @@ pub async fn get_bundles_with_update_status(
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
|
||||||
pub async fn apply_bundle_updates_for_all_clusters(
|
|
||||||
bundles: &BundlesManager,
|
|
||||||
ctx: &ContentCtx,
|
|
||||||
session: Option<&oneclient_events::GroupedProgressSession>,
|
|
||||||
) -> ContentResult<Vec<(i64, ApplyBundleUpdatesResult)>> {
|
|
||||||
let mut changed = Vec::new();
|
|
||||||
for cluster in cluster_dao::list_all(&ctx.db).await? {
|
|
||||||
match apply_bundle_updates_with(cluster.id, bundles, ctx, session, None).await {
|
|
||||||
Ok(result) => {
|
|
||||||
if !result.updates_applied.is_empty()
|
|
||||||
|| !result.additions_applied.is_empty()
|
|
||||||
|| !result.removals_applied.is_empty()
|
|
||||||
|| !result.optional_available.is_empty()
|
|
||||||
{
|
|
||||||
changed.push((cluster.id, result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => tracing::warn!(
|
|
||||||
cluster_id = cluster.id,
|
|
||||||
error = %err,
|
|
||||||
"bundle update apply failed for cluster"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(changed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bundle_package_key(
|
fn bundle_package_key(
|
||||||
bundle_pkg: &BundleTrackedArtifactRow,
|
bundle_pkg: &BundleTrackedArtifactRow,
|
||||||
linked: &HashMap<String, &LinkedArtifactInfo>,
|
linked: &HashMap<String, &LinkedArtifactInfo>,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ use oneclient_db::dao::cluster_bundle as bundle_dao;
|
||||||
use crate::ctx::ContentCtx;
|
use crate::ctx::ContentCtx;
|
||||||
use crate::error::ContentResult;
|
use crate::error::ContentResult;
|
||||||
use crate::packages::dependencies::base_game_version;
|
use crate::packages::dependencies::base_game_version;
|
||||||
use crate::packages::store::PackageStore;
|
use crate::packages::FileIdentity;
|
||||||
|
use crate::packages::store::{PackageStore, record_release};
|
||||||
use crate::packages::types::LinkedArtifactInfo;
|
use crate::packages::types::LinkedArtifactInfo;
|
||||||
|
|
||||||
struct Copy {
|
struct Copy {
|
||||||
|
|
@ -126,11 +127,18 @@ pub async fn disable_foreign_game_versions(
|
||||||
.map(|row| row.hash)
|
.map(|row| row.hash)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let candidates: Vec<&LinkedArtifactInfo> = linked
|
||||||
|
.iter()
|
||||||
|
.filter(|info| {
|
||||||
|
info.enabled && info.content_type == ContentType::Mod && !from_bundle.contains(&info.hash)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
refresh_stated_game_versions(cluster_id, &candidates, mc_version, ctx).await;
|
||||||
|
|
||||||
let mut switched_off = Vec::new();
|
let mut switched_off = Vec::new();
|
||||||
|
|
||||||
for info in linked.iter().filter(|info| {
|
for info in candidates {
|
||||||
info.enabled && info.content_type == ContentType::Mod && !from_bundle.contains(&info.hash)
|
|
||||||
}) {
|
|
||||||
match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await {
|
match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await {
|
||||||
Ok(true) => switched_off.push(
|
Ok(true) => switched_off.push(
|
||||||
info.display_name
|
info.display_name
|
||||||
|
|
@ -152,13 +160,64 @@ pub async fn disable_foreign_game_versions(
|
||||||
|
|
||||||
const FOREIGN_VERSION_REPAIR: &str = "disable-foreign-game-version";
|
const FOREIGN_VERSION_REPAIR: &str = "disable-foreign-game-version";
|
||||||
|
|
||||||
|
fn foreign_repair_id(cluster_id: i64, mc_version: &str, hash: &str) -> String {
|
||||||
|
format!("{FOREIGN_VERSION_REPAIR}:{cluster_id}:{mc_version}:{hash}")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh_stated_game_versions(
|
||||||
|
cluster_id: i64,
|
||||||
|
candidates: &[&LinkedArtifactInfo],
|
||||||
|
mc_version: &str,
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) {
|
||||||
|
let mut stale = Vec::new();
|
||||||
|
|
||||||
|
for info in candidates {
|
||||||
|
match migration_dao::is_applied(&ctx.db, &foreign_repair_id(cluster_id, mc_version, &info.hash)).await {
|
||||||
|
Ok(true) => continue,
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::debug!(hash = %info.hash, %err, "could not read the repair marker");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match artifact_dao::list_release_game_versions(&ctx.db, &info.hash).await {
|
||||||
|
Ok(stated) if built_for_another_game_version(&stated, mc_version) => {
|
||||||
|
stale.push(FileIdentity::from_sha1(&info.hash));
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => tracing::debug!(hash = %info.hash, %err, "could not read the stated game versions"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stale.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = match ctx.providers.lookup_versions(&stale, ctx).await {
|
||||||
|
Ok(found) => found,
|
||||||
|
// Offline or the provider is down, so the stored rows are all there is
|
||||||
|
Err(err) => {
|
||||||
|
tracing::debug!(%err, "could not refresh the stated game versions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (sha1, (provider, version)) in found {
|
||||||
|
if let Err(err) = record_release(provider, &version, &sha1, ctx).await {
|
||||||
|
tracing::debug!(hash = %sha1, %err, "could not store the refreshed game versions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn switch_off_if_foreign(
|
async fn switch_off_if_foreign(
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
info: &LinkedArtifactInfo,
|
info: &LinkedArtifactInfo,
|
||||||
mc_version: &str,
|
mc_version: &str,
|
||||||
ctx: &ContentCtx,
|
ctx: &ContentCtx,
|
||||||
) -> ContentResult<bool> {
|
) -> ContentResult<bool> {
|
||||||
let repair_id = format!("{FOREIGN_VERSION_REPAIR}:{cluster_id}:{mc_version}:{}", info.hash);
|
let repair_id = foreign_repair_id(cluster_id, mc_version, &info.hash);
|
||||||
if migration_dao::is_applied(&ctx.db, &repair_id).await? {
|
if migration_dao::is_applied(&ctx.db, &repair_id).await? {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ async fn already_described(row: &ArtifactRow, ctx: &ContentCtx) -> bool {
|
||||||
|
|
||||||
/// Written exactly as a download would have so the row joins the update flow
|
/// Written exactly as a download would have so the row joins the update flow
|
||||||
/// rather than sitting outside it
|
/// rather than sitting outside it
|
||||||
async fn record_release(
|
pub(crate) async fn record_release(
|
||||||
provider: ProviderId,
|
provider: ProviderId,
|
||||||
version: &VersionDetail,
|
version: &VersionDetail,
|
||||||
hash: &str,
|
hash: &str,
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,28 @@ pub enum GameError {
|
||||||
|
|
||||||
#[error("failed to spawn the game process: {0}")]
|
#[error("failed to spawn the game process: {0}")]
|
||||||
Spawn(String),
|
Spawn(String),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"the wrapper command '{program}' could not be started: {reason}. Check the Wrapper Command setting."
|
||||||
|
)]
|
||||||
|
WrapperSpawn { program: String, reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameError {
|
||||||
|
pub(crate) fn wrapper_spawn(program: &str, err: &std::io::Error) -> Self {
|
||||||
|
let reason = match err.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
"command not found".to_string()
|
||||||
|
}
|
||||||
|
std::io::ErrorKind::PermissionDenied => {
|
||||||
|
"insufficient permissions or file is not an executable".to_string()
|
||||||
|
}
|
||||||
|
_ => err.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::WrapperSpawn {
|
||||||
|
program: program.to_string(),
|
||||||
|
reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use interfrost::api::minecraft::ArgumentType;
|
use interfrost::api::minecraft::ArgumentType;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::ClusterStage;
|
use crate::ClusterStage;
|
||||||
|
|
@ -332,7 +333,13 @@ async fn start(
|
||||||
)?;
|
)?;
|
||||||
arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None);
|
arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None);
|
||||||
|
|
||||||
run_hook(profile.hook_pre.as_deref(), &cwd).await;
|
if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await {
|
||||||
|
events
|
||||||
|
.notify("Pre-launch command failed")
|
||||||
|
.body(reason)
|
||||||
|
.error()
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
|
|
@ -344,7 +351,7 @@ async fn start(
|
||||||
);
|
);
|
||||||
tracing::debug!(cluster_id, ?jvm_args, main_class = %version_info.main_class, "jvm arguments");
|
tracing::debug!(cluster_id, ?jvm_args, main_class = %version_info.main_class, "jvm arguments");
|
||||||
|
|
||||||
let mut command = base_command(&profile, &java.absolute_path);
|
let (mut command, wrapper) = base_command(&profile, &java.absolute_path);
|
||||||
|
|
||||||
if profile.use_discrete_gpu() {
|
if profile.use_discrete_gpu() {
|
||||||
crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await;
|
crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await;
|
||||||
|
|
@ -387,9 +394,10 @@ async fn start(
|
||||||
command.stdin(Stdio::null());
|
command.stdin(Stdio::null());
|
||||||
detach(&mut command);
|
detach(&mut command);
|
||||||
|
|
||||||
let mut child = command
|
let mut child = command.spawn().map_err(|err| match &wrapper {
|
||||||
.spawn()
|
Some(program) => GameError::wrapper_spawn(program, &err),
|
||||||
.map_err(|err| GameError::Spawn(err.to_string()))?;
|
None => GameError::Spawn(err.to_string()),
|
||||||
|
})?;
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
|
|
||||||
stage(LaunchStage::Running);
|
stage(LaunchStage::Running);
|
||||||
|
|
@ -566,7 +574,15 @@ pub(crate) async fn finalize_session(
|
||||||
recorder.finish_at(&end.ended_at.to_rfc3339(), code).await;
|
recorder.finish_at(&end.ended_at.to_rfc3339(), code).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
run_hook(post_hook, cwd).await;
|
if let Some(reason) = run_hook(post_hook, cwd).await {
|
||||||
|
state
|
||||||
|
.services
|
||||||
|
.events
|
||||||
|
.notify("Post-exit command failed")
|
||||||
|
.body(reason)
|
||||||
|
.error()
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
if dedicated {
|
if dedicated {
|
||||||
// The folder stays materialized so it remains a real Minecraft directory
|
// The folder stays materialized so it remains a real Minecraft directory
|
||||||
|
|
@ -667,21 +683,25 @@ pub async fn offer_repair(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_command(profile: &GameSettingsProfile, java_path: &str) -> Command {
|
fn base_command(profile: &GameSettingsProfile, java_path: &str) -> (Command, Option<String>) {
|
||||||
if let Some(wrapper) = profile
|
let Some(wrapper) = profile
|
||||||
.hook_wrapper
|
.hook_wrapper
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|hook| !hook.is_empty())
|
.filter(|hook| !hook.is_empty())
|
||||||
{
|
else {
|
||||||
|
return (Command::new(java_path), None);
|
||||||
|
};
|
||||||
|
|
||||||
let mut split = wrapper.split_whitespace();
|
let mut split = wrapper.split_whitespace();
|
||||||
let mut command = Command::new(split.next().unwrap_or("sh"));
|
let Some(program) = split.next() else {
|
||||||
|
return (Command::new(java_path), None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut command = Command::new(program);
|
||||||
command.args(split);
|
command.args(split);
|
||||||
command.arg(java_path);
|
command.arg(java_path);
|
||||||
command
|
(command, Some(program.to_string()))
|
||||||
} else {
|
|
||||||
Command::new(java_path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
||||||
|
|
@ -697,10 +717,8 @@ fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(cwd), fields(hook), level = "debug")]
|
#[tracing::instrument(skip(cwd), fields(hook), level = "debug")]
|
||||||
async fn run_hook(hook: Option<&str>, cwd: &Path) {
|
async fn run_hook(hook: Option<&str>, cwd: &Path) -> Option<String> {
|
||||||
let Some(hook) = hook.map(str::trim).filter(|h| !h.is_empty()) else {
|
let hook = hook.map(str::trim).filter(|h| !h.is_empty())?;
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let mut command = {
|
let mut command = {
|
||||||
|
|
@ -716,12 +734,84 @@ async fn run_hook(hook: Option<&str>, cwd: &Path) {
|
||||||
};
|
};
|
||||||
|
|
||||||
command.current_dir(cwd);
|
command.current_dir(cwd);
|
||||||
|
command.stderr(Stdio::piped());
|
||||||
|
command.stdin(Stdio::null());
|
||||||
oneclient_common::process::no_window(command.as_std_mut());
|
oneclient_common::process::no_window(command.as_std_mut());
|
||||||
if let Err(err) = command.status().await {
|
|
||||||
tracing::warn!("hook '{hook}' failed: {err}");
|
let mut child = match command.spawn() {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("hook '{hook}' could not start: {err}");
|
||||||
|
return Some(format!("The command shell could not be started: {err}"));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let collector = child.stderr.take().map(|mut pipe| {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let _ = pipe.read_to_end(&mut buf).await;
|
||||||
|
buf
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = match child.wait().await {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(err) => {
|
||||||
|
if let Some(collector) = collector {
|
||||||
|
collector.abort();
|
||||||
|
}
|
||||||
|
tracing::warn!("hook '{hook}' could not be waited on: {err}");
|
||||||
|
return Some(format!("The command could not be waited on: {err}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
if let Some(collector) = collector {
|
||||||
|
collector.abort();
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let detail = match collector {
|
||||||
|
Some(mut collector) => {
|
||||||
|
match tokio::time::timeout(Duration::from_millis(200), &mut collector).await {
|
||||||
|
Ok(Ok(buf)) => stderr_detail(&buf),
|
||||||
|
_ => {
|
||||||
|
collector.abort();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = match (detail, status.code()) {
|
||||||
|
(Some(detail), _) => detail,
|
||||||
|
(None, Some(code)) => format!("The command exited with code {code}"),
|
||||||
|
(None, None) => "The command was stopped before it finished".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::warn!("hook '{hook}' failed: {message}");
|
||||||
|
Some(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stderr_detail(bytes: &[u8]) -> Option<String> {
|
||||||
|
let text = String::from_utf8_lossy(bytes);
|
||||||
|
let line = text
|
||||||
|
.lines()
|
||||||
|
.rev()
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|line| !line.is_empty())?;
|
||||||
|
|
||||||
|
const MAX: usize = 300;
|
||||||
|
if line.chars().count() <= MAX {
|
||||||
|
return Some(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut clipped: String = line.chars().take(MAX).collect();
|
||||||
|
clipped.push('…');
|
||||||
|
Some(clipped)
|
||||||
|
}
|
||||||
/// Computes Java's exact 32-bit signed integer `uuid.hashCode()`.
|
/// Computes Java's exact 32-bit signed integer `uuid.hashCode()`.
|
||||||
fn java_uuid_hash(uuid: &uuid::Uuid) -> i32 {
|
fn java_uuid_hash(uuid: &uuid::Uuid) -> i32 {
|
||||||
let (most_sig, least_sig) = uuid.as_u64_pair();
|
let (most_sig, least_sig) = uuid.as_u64_pair();
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub use oneclient_content::bundles::{
|
||||||
cluster_has_bundle_content, effective_enabled,
|
cluster_has_bundle_content, effective_enabled,
|
||||||
install_bundle,
|
install_bundle,
|
||||||
install_cluster_bundles,
|
install_cluster_bundles,
|
||||||
install_package_from_bundle, is_bundle_syncing, list_cluster_bundle_overrides,
|
install_package_from_bundle, list_cluster_bundle_overrides,
|
||||||
pending_optional_mods, resolve_optional_mods, skip_optional_mods, PendingOptionalMod,
|
pending_optional_mods, resolve_optional_mods, skip_optional_mods, PendingOptionalMod,
|
||||||
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
||||||
set_bundle_package_overrides,
|
set_bundle_package_overrides,
|
||||||
|
|
|
||||||