From 80d66d3c6eea5a8bef1aee39d9f5a3e8b390f65e Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:13:42 -0700 Subject: [PATCH 1/6] update ads.txt --- apps/frontend/src/public/ads.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/public/ads.txt b/apps/frontend/src/public/ads.txt index 6621397bca..a9fad9d4dd 100644 --- a/apps/frontend/src/public/ads.txt +++ b/apps/frontend/src/public/ads.txt @@ -526,4 +526,11 @@ yahoo.com, 100033, DIRECT, e1a5b5b6e3255540 #IndexExchange indexexchange.com, 184169, DIRECT, 50b1c356f2c5c8fc -indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc \ No newline at end of file +indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc + +#AppNexus +Xandr.com, 2398, DIRECT + +#Rubicon +rubiconproject.com, 24584, DIRECT, 0bfd66d529a55807 +rubiconproject.com, 24586, DIRECT, 0bfd66d529a55807 From 5fe07de187046bb6129fd88e727b53b9faf63447 Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Fri, 26 Jun 2026 17:22:46 +0200 Subject: [PATCH 2/6] chore: bump mold and add mold to other jobs --- .github/workflows/daedalus-docker.yml | 3 +++ .github/workflows/theseus-build.yml | 4 ++++ .github/workflows/turbo-ci.yml | 3 +++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/daedalus-docker.yml b/.github/workflows/daedalus-docker.yml index 3f11eb4edb..c401860968 100644 --- a/.github/workflows/daedalus-docker.yml +++ b/.github/workflows/daedalus-docker.yml @@ -96,6 +96,9 @@ jobs: rustflags: '' cache: false + - name: Setup mold + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 + - name: Cache Cargo registry and index uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 3c0f7b1384..4c7ad48e46 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -84,6 +84,10 @@ jobs: rustflags: '' target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }} + - name: Setup mold + if: contains(matrix.platform, 'ubuntu') + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 + - name: Setup sccache uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 diff --git a/.github/workflows/turbo-ci.yml b/.github/workflows/turbo-ci.yml index db0340dae4..c0cd950d99 100644 --- a/.github/workflows/turbo-ci.yml +++ b/.github/workflows/turbo-ci.yml @@ -129,6 +129,9 @@ jobs: components: clippy, rustfmt cache: false + - name: Setup mold + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 + - name: Cache Cargo registry and index uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 with: From e4376c2b15127641706c727805577d42aac6243f Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 26 Jun 2026 16:50:03 +0100 Subject: [PATCH 3/6] fix: clamp negative playtime (#6519) * fix: clamp negative playtime * fix: remove triggers use instant --- apps/app-frontend/src/App.vue | 11 ++-- ...0249bdc4023c0b3d0c683318079ce93c9dd56.json | 12 ++++ ...8587c035eb94098831c690739393870d4e814.json | 12 ---- ...c9c425cfac0520785d4e25d687c2d9ea82088.json | 12 ---- ...c2f984ce07a6d9498f19c24c6e2c031122fc5.json | 12 ++++ ...60000_clamp-negative-instance-playtime.sql | 12 ++++ .../adapters/sqlite/instance_rows.rs | 18 ++++-- .../instances/commands/launch_context.rs | 39 +++++++++++-- .../src/state/instances/model/instance.rs | 12 ++++ .../app-lib/src/state/legacy_converter.rs | 10 +++- packages/app-lib/src/state/process.rs | 55 ++++++++++--------- 11 files changed, 135 insertions(+), 70 deletions(-) create mode 100644 packages/app-lib/.sqlx/query-3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56.json delete mode 100644 packages/app-lib/.sqlx/query-44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814.json delete mode 100644 packages/app-lib/.sqlx/query-8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088.json create mode 100644 packages/app-lib/.sqlx/query-d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5.json create mode 100644 packages/app-lib/migrations/20260626160000_clamp-negative-instance-playtime.sql diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 83e2312c44..1f39661978 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -1341,12 +1341,11 @@ async function processPendingSurveys() { const creds = await getCreds().catch(handleError) const userId = creds?.user_id - const instances = await list().catch(handleError) - const isActivePlayer = - instances.findIndex( - (instance) => - isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created), - ) >= 0 + const instances = (await list().catch(handleError)) ?? [] + const isActivePlayer = instances.some( + (instance) => + isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created), + ) let surveys = [] try { diff --git a/packages/app-lib/.sqlx/query-3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56.json b/packages/app-lib/.sqlx/query-3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56.json new file mode 100644 index 0000000000..105a1c239c --- /dev/null +++ b/packages/app-lib/.sqlx/query-3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = CASE\n\t\t\t\tWHEN submitted_time_played < 0 THEN ?\n\t\t\t\tWHEN submitted_time_played > ? THEN ?\n\t\t\t\tELSE submitted_time_played + ?\n\t\t\tEND,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56" +} diff --git a/packages/app-lib/.sqlx/query-44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814.json b/packages/app-lib/.sqlx/query-44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814.json deleted file mode 100644 index f52de035a3..0000000000 --- a/packages/app-lib/.sqlx/query-44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = submitted_time_played + ?,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814" -} diff --git a/packages/app-lib/.sqlx/query-8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088.json b/packages/app-lib/.sqlx/query-8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088.json deleted file mode 100644 index 64eb7c2150..0000000000 --- a/packages/app-lib/.sqlx/query-8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n\t\tUPDATE instances\n\t\tSET recent_time_played = recent_time_played + ?, modified = ?\n\t\tWHERE id = ?\n\t\t", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088" -} diff --git a/packages/app-lib/.sqlx/query-d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5.json b/packages/app-lib/.sqlx/query-d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5.json new file mode 100644 index 0000000000..9e1357f74c --- /dev/null +++ b/packages/app-lib/.sqlx/query-d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\trecent_time_played = CASE\n\t\t\t\tWHEN recent_time_played < 0 THEN ?\n\t\t\t\tWHEN recent_time_played > ? THEN ?\n\t\t\t\tELSE recent_time_played + ?\n\t\t\tEND,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5" +} diff --git a/packages/app-lib/migrations/20260626160000_clamp-negative-instance-playtime.sql b/packages/app-lib/migrations/20260626160000_clamp-negative-instance-playtime.sql new file mode 100644 index 0000000000..d5a624f251 --- /dev/null +++ b/packages/app-lib/migrations/20260626160000_clamp-negative-instance-playtime.sql @@ -0,0 +1,12 @@ +UPDATE instances +SET + submitted_time_played = CASE + WHEN submitted_time_played < 0 THEN 0 + ELSE submitted_time_played + END, + recent_time_played = CASE + WHEN recent_time_played < 0 THEN 0 + ELSE recent_time_played + END +WHERE submitted_time_played < 0 + OR recent_time_played < 0; diff --git a/packages/app-lib/src/state/instances/adapters/sqlite/instance_rows.rs b/packages/app-lib/src/state/instances/adapters/sqlite/instance_rows.rs index 0ed985183a..5a4a0e6141 100644 --- a/packages/app-lib/src/state/instances/adapters/sqlite/instance_rows.rs +++ b/packages/app-lib/src/state/instances/adapters/sqlite/instance_rows.rs @@ -3,7 +3,7 @@ use crate::state::instances::{ ContentSet, ContentSetStatus, ContentSourceKind, Instance, InstanceLaunchContext, InstanceLaunchOverrides, - InstanceLaunchOverridesData, InstanceLink, + InstanceLaunchOverridesData, InstanceLink, playtime_to_storage, }; use crate::state::{ InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel, @@ -831,8 +831,12 @@ pub(crate) async fn insert_instance( let created = instance.created.timestamp(); let modified = instance.modified.timestamp(); let last_played = instance.last_played.map(|value| value.timestamp()); - let submitted_time_played = instance.submitted_time_played as i64; - let recent_time_played = instance.recent_time_played as i64; + let submitted_time_played = playtime_to_storage( + instance.submitted_time_played, + "submitted_time_played", + )?; + let recent_time_played = + playtime_to_storage(instance.recent_time_played, "recent_time_played")?; sqlx::query!( " @@ -887,8 +891,12 @@ pub(crate) async fn update_instance( let icon_path = instance.icon_path.as_deref(); let modified = instance.modified.timestamp(); let last_played = instance.last_played.map(|value| value.timestamp()); - let submitted_time_played = instance.submitted_time_played as i64; - let recent_time_played = instance.recent_time_played as i64; + let submitted_time_played = playtime_to_storage( + instance.submitted_time_played, + "submitted_time_played", + )?; + let recent_time_played = + playtime_to_storage(instance.recent_time_played, "recent_time_played")?; sqlx::query!( " diff --git a/packages/app-lib/src/state/instances/commands/launch_context.rs b/packages/app-lib/src/state/instances/commands/launch_context.rs index d37b070c9e..6b600a97d9 100644 --- a/packages/app-lib/src/state/instances/commands/launch_context.rs +++ b/packages/app-lib/src/state/instances/commands/launch_context.rs @@ -1,6 +1,6 @@ use crate::state::InstanceInstallStage; use crate::state::instances::{ - InstanceLaunchContext, adapters::sqlite::instance_rows, + InstanceLaunchContext, adapters::sqlite::instance_rows, playtime_to_storage, }; use chrono::{DateTime, Utc}; use sqlx::SqlitePool; @@ -120,16 +120,31 @@ pub(crate) async fn add_instance_recent_playtime( seconds: u64, pool: &SqlitePool, ) -> crate::Result<()> { - let seconds = seconds as i64; + if seconds == 0 { + return Ok(()); + } + + let seconds = playtime_to_storage(seconds, "recent_time_played")?; + let max_playtime = i64::MAX; + let max_playtime_before_increment = max_playtime - seconds; let modified = Utc::now().timestamp(); sqlx::query!( " UPDATE instances - SET recent_time_played = recent_time_played + ?, modified = ? + SET + recent_time_played = CASE + WHEN recent_time_played < 0 THEN ? + WHEN recent_time_played > ? THEN ? + ELSE recent_time_played + ? + END, + modified = ? WHERE id = ? ", seconds, + max_playtime_before_increment, + max_playtime, + seconds, modified, instance_id, ) @@ -144,19 +159,33 @@ pub(crate) async fn mark_instance_playtime_submitted( recent_time_played: u64, pool: &SqlitePool, ) -> crate::Result<()> { - let recent_time_played = recent_time_played as i64; + if recent_time_played == 0 { + return Ok(()); + } + + let recent_time_played = + playtime_to_storage(recent_time_played, "recent_time_played")?; + let max_playtime = i64::MAX; + let max_playtime_before_increment = max_playtime - recent_time_played; let modified = Utc::now().timestamp(); sqlx::query!( " UPDATE instances SET - submitted_time_played = submitted_time_played + ?, + submitted_time_played = CASE + WHEN submitted_time_played < 0 THEN ? + WHEN submitted_time_played > ? THEN ? + ELSE submitted_time_played + ? + END, recent_time_played = 0, modified = ? WHERE id = ? ", recent_time_played, + max_playtime_before_increment, + max_playtime, + recent_time_played, modified, instance_id, ) diff --git a/packages/app-lib/src/state/instances/model/instance.rs b/packages/app-lib/src/state/instances/model/instance.rs index 4bfda092f6..506d0c3516 100644 --- a/packages/app-lib/src/state/instances/model/instance.rs +++ b/packages/app-lib/src/state/instances/model/instance.rs @@ -20,3 +20,15 @@ pub struct Instance { pub submitted_time_played: u64, pub recent_time_played: u64, } + +pub(crate) fn playtime_to_storage( + value: u64, + column: &str, +) -> crate::Result { + i64::try_from(value).map_err(|_| { + crate::ErrorKind::InputError(format!( + "Expected {column} to fit in SQLite INTEGER" + )) + .into() + }) +} diff --git a/packages/app-lib/src/state/legacy_converter.rs b/packages/app-lib/src/state/legacy_converter.rs index ba9db4aace..b602a09b3a 100644 --- a/packages/app-lib/src/state/legacy_converter.rs +++ b/packages/app-lib/src/state/legacy_converter.rs @@ -3,7 +3,7 @@ use crate::jre::check_jre; use crate::prelude::ModLoader; use crate::state; use crate::state::instances::{ - InstanceLaunchOverrides, InstanceLaunchOverridesData, + InstanceLaunchOverrides, InstanceLaunchOverridesData, playtime_to_storage, }; use crate::state::{ CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate, @@ -443,8 +443,12 @@ where let created = input.created.timestamp(); let modified = input.modified.timestamp(); let last_played = input.last_played.map(|value| value.timestamp()); - let submitted_time_played = input.submitted_time_played as i64; - let recent_time_played = input.recent_time_played as i64; + let submitted_time_played = playtime_to_storage( + input.submitted_time_played, + "submitted_time_played", + )?; + let recent_time_played = + playtime_to_storage(input.recent_time_played, "recent_time_played")?; sqlx::query!( " diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index cc2aff0224..669dac581f 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -17,6 +17,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use std::sync::LazyLock; +use std::time::Instant; #[cfg(feature = "tauri")] use tauri::Emitter; use tempfile::TempDir; @@ -752,46 +753,46 @@ impl Process { uuid: Uuid, ) -> crate::Result<()> { async fn update_playtime( - last_updated_playtime: &mut DateTime, + last_updated_playtime: &mut Instant, instance_id: &str, force_update: bool, ) { - let diff = Utc::now() - .signed_duration_since(*last_updated_playtime) - .num_seconds(); - if diff >= 60 || force_update { - let state = match crate::State::get().await { - Ok(state) => state, - Err(e) => { - tracing::warn!( - "Failed to get state for playtime update on instance {}: {}", - instance_id, - e - ); - return; - } - }; - if let Err(e) = - crate::state::instances::commands::add_instance_recent_playtime( - instance_id, - diff as u64, - &state.pool, - ) - .await - { + let elapsed = last_updated_playtime.elapsed().as_secs(); + if elapsed == 0 || (!force_update && elapsed < 60) { + return; + } + + let state = match crate::State::get().await { + Ok(state) => state, + Err(e) => { tracing::warn!( - "Failed to update playtime for instance {}: {}", + "Failed to get state for playtime update on instance {}: {}", instance_id, e ); + return; } - *last_updated_playtime = Utc::now(); + }; + if let Err(e) = + crate::state::instances::commands::add_instance_recent_playtime( + instance_id, + elapsed, + &state.pool, + ) + .await + { + tracing::warn!( + "Failed to update playtime for instance {}: {}", + instance_id, + e + ); } + *last_updated_playtime = Instant::now(); } // Wait on current Minecraft Child let mc_exit_status; - let mut last_updated_playtime = Utc::now(); + let mut last_updated_playtime = Instant::now(); let state = crate::State::get().await?; loop { From ebf4a6ef5f6b8ed2ac5d134d3c116e522570be26 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 26 Jun 2026 16:50:15 +0100 Subject: [PATCH 4/6] fix: bulk update updating linked content (#6517) * fix: bulk update updating linked content * fix: disabled content sometimes desyncing * fix: bad filter * fix: fmt --- apps/app-frontend/src/helpers/types.d.ts | 1 + ...384564c86cd98a1e407515ea718127a3b1777.json | 12 ++++++ ...93eb92fa6199c7356bbc2c5ca36468f916380.json | 12 ------ packages/app-lib/src/state/instance_types.rs | 1 + .../instances/adapters/sqlite/content_rows.rs | 7 ++-- .../commands/apply_content_install.rs | 20 +++++++++- .../commands/apply_content_update.rs | 38 ++++++++++++++++++- .../state/instances/commands/list_content.rs | 28 +++++++++++++- .../instances/commands/sync_content_files.rs | 17 +++++++-- 9 files changed, 114 insertions(+), 22 deletions(-) create mode 100644 packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json delete mode 100644 packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index 646a0b47d0..0bd0101461 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -97,6 +97,7 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha' export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' type ContentFile = { + enabled: boolean metadata?: { project_id: string version_id: string diff --git a/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json b/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json new file mode 100644 index 0000000000..97e1f9d045 --- /dev/null +++ b/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777" +} diff --git a/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json b/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json deleted file mode 100644 index c28f2d9854..0000000000 --- a/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380" -} diff --git a/packages/app-lib/src/state/instance_types.rs b/packages/app-lib/src/state/instance_types.rs index 8db72d4525..4fefab25a1 100644 --- a/packages/app-lib/src/state/instance_types.rs +++ b/packages/app-lib/src/state/instance_types.rs @@ -117,6 +117,7 @@ impl ModLoader { pub struct ContentFile { pub hash: String, pub file_name: String, + pub enabled: bool, pub size: u64, pub metadata: Option, pub update_version_id: Option, diff --git a/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs b/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs index 58e3071968..e8b6e108a5 100644 --- a/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs +++ b/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs @@ -627,6 +627,7 @@ pub(crate) async fn rename_instance_file( relative_path = ?, file_name = ?, enabled = ?, + missing = 0, modified_at = ? WHERE instance_id = ? AND relative_path = ? ", @@ -856,11 +857,11 @@ pub(crate) async fn set_content_entry_enabled_for_file( file_id: &str, enabled: bool, pool: &SqlitePool, -) -> crate::Result<()> { +) -> crate::Result { let enabled = i64::from(enabled); let modified_at = Utc::now().timestamp(); - sqlx::query!( + let result = sqlx::query!( " UPDATE instance_content_entries SET enabled = ?, modified_at = ? @@ -874,7 +875,7 @@ pub(crate) async fn set_content_entry_enabled_for_file( .execute(pool) .await?; - Ok(()) + Ok(result.rows_affected() > 0) } pub(crate) async fn remove_content_entries_for_file( diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 20e4e16101..0c780c7e76 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -661,13 +661,31 @@ pub(crate) async fn toggle_disable_project( } None => index_existing_file(&scope, &new_path, state).await?, }; - content_rows::set_content_entry_enabled_for_file( + let updated_entry = content_rows::set_content_entry_enabled_for_file( &scope.content_set_id, &file.id, enabled, &state.pool, ) .await?; + if !updated_entry { + let project_type = ProjectType::get_from_parent_folder(&new_path) + .ok_or_else(|| { + crate::ErrorKind::InputError(format!( + "Unable to infer project type from {new_path}" + )) + })?; + upsert_entry_for_file( + &scope, + &file, + project_type, + None, + None, + ContentSourceKind::Local, + &state.pool, + ) + .await?; + } Ok(new_path) } diff --git a/packages/app-lib/src/state/instances/commands/apply_content_update.rs b/packages/app-lib/src/state/instances/commands/apply_content_update.rs index 42bca46c1b..e89d5b5378 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_update.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_update.rs @@ -281,12 +281,31 @@ async fn plan_bulk_update( instance_id: &str, state: &State, ) -> crate::Result { + let updateable_paths = + bulk_updateable_project_paths(instance_id, state).await?; + if updateable_paths.is_empty() { + return Ok(BulkUpdatePlan { + project_updates: Vec::new(), + dependency_additions: Vec::new(), + }); + } + let updates = check_content_updates( instance_id, Some(CacheBehaviour::MustRevalidate), state, ) - .await?; + .await? + .into_iter() + .filter(|update| updateable_paths.contains(&update.relative_path)) + .collect::>(); + if updates.is_empty() { + return Ok(BulkUpdatePlan { + project_updates: Vec::new(), + dependency_additions: Vec::new(), + }); + } + let content_set = content_rows::get_applied_content_set(instance_id, &state.pool) .await? @@ -320,6 +339,7 @@ async fn plan_bulk_update( .collect::>(); let version_ids = installed .iter() + .filter(|project| updateable_paths.contains(&project.relative_path)) .filter_map(|project| project.version_id.clone()) .chain( updates @@ -343,6 +363,7 @@ async fn plan_bulk_update( let planned_versions = installed .iter() .filter(|project| project.enabled) + .filter(|project| updateable_paths.contains(&project.relative_path)) .filter_map(|project| { let target_version_id = updates_by_path .get(&project.relative_path) @@ -379,6 +400,21 @@ async fn plan_bulk_update( }) } +async fn bulk_updateable_project_paths( + instance_id: &str, + state: &State, +) -> crate::Result> { + let items = super::list_content::list_content( + instance_id, + None, + Some(CacheBehaviour::MustRevalidate), + state, + ) + .await?; + + Ok(items.into_iter().map(|item| item.file_path).collect()) +} + async fn installed_projects( instance_id: &str, content_set: &ContentSet, diff --git a/packages/app-lib/src/state/instances/commands/list_content.rs b/packages/app-lib/src/state/instances/commands/list_content.rs index cd58d74902..529efff676 100644 --- a/packages/app-lib/src/state/instances/commands/list_content.rs +++ b/packages/app-lib/src/state/instances/commands/list_content.rs @@ -208,6 +208,8 @@ pub(crate) async fn list_content( ) .await?; let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link); + let linked_modpack_source_kind = linked_modpack_source_kind(&link); + let mut failed_modpack_identifier_lookup = false; let modpack_ids = if imported_modpack_scope { None } else { @@ -226,6 +228,7 @@ pub(crate) async fn list_content( "Failed to fetch modpack identifiers: {}", err ); + failed_modpack_identifier_lookup = true; None } }, @@ -240,6 +243,12 @@ pub(crate) async fn list_content( } } else if let Some(ids) = modpack_ids.as_ref() { ContentFilter::ExcludeModpack(ids) + } else if failed_modpack_identifier_lookup { + ContentFilter::ExcludeSourceKind { + source_kind: linked_modpack_source_kind + .unwrap_or(ContentSourceKind::ModrinthModpack), + exclude_untracked: true, + } } else { ContentFilter::All }; @@ -735,6 +744,9 @@ async fn content_projects_for_scope( update_version_id, hash: file.sha1, file_name: file.file_name, + enabled: entry.map_or(file.enabled, |entry| { + entry.enabled && file.enabled + }), size: file.size, metadata: file_metadata_from_entry_or_cache(entry, metadata), project_type, @@ -882,7 +894,7 @@ async fn content_files_to_content_items( file_path: path.clone(), id: file.hash.clone(), size: file.size, - enabled: !file.file_name.ends_with(".disabled"), + enabled: file.enabled, project_type: file.project_type, project: project.map(|project| ContentItemProject { id: project.id.clone(), @@ -1097,6 +1109,20 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> { } } +fn linked_modpack_source_kind( + link: &InstanceLink, +) -> Option { + match link { + InstanceLink::ModrinthModpack { .. } => { + Some(ContentSourceKind::ModrinthModpack) + } + InstanceLink::ServerProjectModpack { .. } => { + Some(ContentSourceKind::ServerProject) + } + _ => None, + } +} + fn check_modpack_update( installed_version_id: &str, installed_version: &Version, diff --git a/packages/app-lib/src/state/instances/commands/sync_content_files.rs b/packages/app-lib/src/state/instances/commands/sync_content_files.rs index 4dada1c645..3686558393 100644 --- a/packages/app-lib/src/state/instances/commands/sync_content_files.rs +++ b/packages/app-lib/src/state/instances/commands/sync_content_files.rs @@ -39,9 +39,18 @@ pub(crate) async fn sync_instance_content_files( &state.api_semaphore, ) .await?; - let hashes_by_path = hashes + let hashes_by_key = hashes .into_iter() - .map(|hash| (hash.path.clone(), hash)) + .map(|hash| { + ( + format!( + "{}-{}", + hash.size, + hash.path.trim_end_matches(".disabled") + ), + hash, + ) + }) .collect::>(); let existing_files = sqlite::content_rows::get_instance_files(&instance.id, &state.pool) @@ -55,8 +64,8 @@ pub(crate) async fn sync_instance_content_files( let mut files = Vec::new(); for file in scanned { - let cache_path = format!("{}/{}", instance.path, file.relative_path); - let Some(hash) = hashes_by_path.get(&cache_path) else { + let hash_key = file.hash_cache_key.trim_end_matches(".disabled"); + let Some(hash) = hashes_by_key.get(hash_key) else { continue; }; let existing_file = existing_files_by_path.get(&file.relative_path); From 1067b02830545bfd3e70093692e89139377506ca Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 26 Jun 2026 16:50:26 +0100 Subject: [PATCH 5/6] fix: logs using id not path (#6511) * fix: logs using id not path * fix: lint --- apps/app-playground/src/main.rs | 2 +- packages/app-lib/src/api/logs.rs | 33 ++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/app-playground/src/main.rs b/apps/app-playground/src/main.rs index 22f964f842..0a7c959884 100644 --- a/apps/app-playground/src/main.rs +++ b/apps/app-playground/src/main.rs @@ -47,7 +47,7 @@ async fn main() -> theseus::Result<()> { for world in worlds { println!( "World: {:?}/{:?} played at {:?}: {:#?}", - world.profile, + world.instance_id, world.world.name, world.world.last_played, world.world.details diff --git a/packages/app-lib/src/api/logs.rs b/packages/app-lib/src/api/logs.rs index 0c383dc68c..4ce0c572d6 100644 --- a/packages/app-lib/src/api/logs.rs +++ b/packages/app-lib/src/api/logs.rs @@ -250,9 +250,15 @@ impl Logs { output: if clear_contents.unwrap_or(false) { None } else { + let state = State::get().await?; Some( - get_output_by_filename(instance_path, log_type, &filename) - .await?, + get_output_by_filename_from_path( + &state, + instance_path, + log_type, + &filename, + ) + .await?, ) }, filename, @@ -358,14 +364,12 @@ pub async fn get_logs_by_filename( Logs::build(log_type, age, &instance_path, filename, Some(true)).await } -#[tracing::instrument] -pub async fn get_output_by_filename( +async fn get_output_by_filename_from_path( + state: &State, instance_path: &str, log_type: LogType, file_name: &str, ) -> crate::Result { - let state = State::get().await?; - let logs_folder = match log_type { LogType::InfoLog => state.directories.instance_logs_dir(instance_path), LogType::CrashReport => { @@ -409,6 +413,23 @@ pub async fn get_output_by_filename( .into()) } +#[tracing::instrument] +pub async fn get_output_by_filename( + instance_id: &str, + log_type: LogType, + file_name: &str, +) -> crate::Result { + let state = State::get().await?; + let instance_path = resolve_instance_path(instance_id, &state).await?; + get_output_by_filename_from_path( + &state, + &instance_path, + log_type, + file_name, + ) + .await +} + #[tracing::instrument] pub async fn delete_logs(instance_id: &str) -> crate::Result<()> { let state = State::get().await?; From b95a2d875a814a603ff39e110532358cc6b5446e Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Fri, 26 Jun 2026 17:42:19 +0100 Subject: [PATCH 6/6] feat: changelog app 0.15.1 (#6522) --- packages/blog/changelog.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/blog/changelog.ts b/packages/blog/changelog.ts index f9adf3a484..5ef90144ba 100644 --- a/packages/blog/changelog.ts +++ b/packages/blog/changelog.ts @@ -10,6 +10,16 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2026-06-26T16:33:23+00:00`, + product: 'app', + version: '0.15.1', + body: `## Fixed +- Fixed issue where previous logs would not load in the Logs tab of an instance. +- Fixed issue where "Update all" would sometimes update linked modpack content. +- Fixed issue where disabled additional content would sometimes not appear as disabled in the UI. +- Fixed issue where instances with negative playtime fail to load. (How?)`, + }, { date: `2026-06-25T21:22:14+00:00`, product: 'app',