wipe off incompatible mc version check entirely

This commit is contained in:
Julian Chang 2026-09-30 06:12:11 -04:00
parent c9c4c9536b
commit e1be4c8bcc
No known key found for this signature in database
4 changed files with 4 additions and 302 deletions

View file

@ -2,19 +2,13 @@
//! of one package are legal rows
//! the newest stays enabled the rest are switched off rather than unlinked
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use oneclient_common::domain::{ContentType, ProviderId};
use oneclient_db::dao::applied_migration as migration_dao;
use oneclient_db::dao::artifact as artifact_dao;
use oneclient_db::dao::bundle as bundle_catalog_dao;
use oneclient_db::dao::cluster_bundle as bundle_dao;
use oneclient_common::domain::ProviderId;
use crate::ctx::ContentCtx;
use crate::error::ContentResult;
use crate::packages::FileIdentity;
use crate::packages::dependencies::base_game_version;
use crate::packages::store::{PackageStore, record_release};
use crate::packages::store::PackageStore;
use crate::packages::types::LinkedArtifactInfo;
struct Copy {
@ -93,180 +87,6 @@ fn group_duplicates(linked: &[LinkedArtifactInfo]) -> Vec<((ProviderId, String),
by_project.into_iter().collect()
}
#[tracing::instrument(level = "debug", skip(ctx))]
pub async fn disable_foreign_game_versions(
cluster_id: i64,
mc_version: &str,
mc_loader: i64,
ctx: &ContentCtx,
) -> ContentResult<Vec<String>> {
if mc_version.is_empty() {
return Ok(Vec::new());
}
let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?;
// bundle installs skip the compatibility check so launch must trust them too
let live_bundles: HashSet<String> =
bundle_catalog_dao::list_visible_for_version_loader(&ctx.db, mc_version, mc_loader)
.await?
.into_iter()
.filter_map(|row| row.name)
.collect();
let from_bundle: HashSet<String> = bundle_dao::list_bundle_tracked(&ctx.db, cluster_id)
.await?
.into_iter()
.filter(|row| {
row.bundle_name
.as_deref()
.is_some_and(|name| live_bundles.contains(name))
})
.map(|row| row.hash)
.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();
for info in candidates {
match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await {
Ok(true) => switched_off.push(
info.display_name
.clone()
.unwrap_or_else(|| info.file_name.clone()),
),
Ok(false) => {}
Err(err) => tracing::warn!(
cluster_id,
hash = %info.hash,
%err,
"could not switch off a mod built for another game version"
),
}
}
Ok(switched_off)
}
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(
cluster_id: i64,
info: &LinkedArtifactInfo,
mc_version: &str,
ctx: &ContentCtx,
) -> ContentResult<bool> {
let repair_id = foreign_repair_id(cluster_id, mc_version, &info.hash);
if migration_dao::is_applied(&ctx.db, &repair_id).await? {
return Ok(false);
}
let stated = artifact_dao::list_release_game_versions(&ctx.db, &info.hash).await?;
if !built_for_another_game_version(&stated, mc_version) {
return Ok(false);
}
tracing::info!(
cluster_id,
hash = %info.hash,
built_for = ?stated,
"switching off a mod built for another game version"
);
PackageStore::set_artifact_enabled_to(cluster_id, &info.hash, false, ctx).await?;
migration_dao::mark_applied(&ctx.db, &repair_id).await?;
Ok(true)
}
fn built_for_another_game_version(stated: &[String], mc_version: &str) -> bool {
!stated.is_empty()
&& !stated.iter().any(|versions| {
let built_for: Vec<String> = serde_json::from_str(versions).unwrap_or_default();
covers_game_version(&built_for, mc_version)
})
}
fn covers_game_version(stated: &[String], mc_version: &str) -> bool {
let wanted = base_game_version(mc_version);
stated.is_empty()
|| stated.iter().any(|version| {
let built_for = base_game_version(version);
built_for == wanted
|| wanted
.strip_prefix(built_for)
.is_some_and(|rest| rest.starts_with('.'))
})
}
/// `published_at` is RFC 3339 so string order matches time order
/// undated copies lose to dated ones and an all-undated group still picks one
fn newest<'a>(copies: impl IntoIterator<Item = &'a Copy>) -> Option<String> {
@ -350,78 +170,6 @@ mod tests {
);
}
#[test]
fn only_a_stated_mismatch_switches_a_mod_off() {
let foreign = |rows: &[&str]| {
built_for_another_game_version(
&rows.iter().map(|r| (*r).to_string()).collect::<Vec<_>>(),
"26.1.2",
)
};
assert!(
!foreign(&[]),
"a jar no provider ever described is left alone"
);
assert!(
!foreign(&["[]"]),
"an empty list is the provider saying nothing"
);
assert!(
!foreign(&["[\"1.21.11"]),
"a list that will not parse says nothing"
);
assert!(!foreign(&["[\"26.1.2\"]"]));
assert!(
!foreign(&["[\"1.21.11\",\"26.1.2\"]"]),
"a multi-version build stays"
);
assert!(
!foreign(&["[\"1.21.11\"]", "[\"26.1.2\"]"]),
"one release row out of two is enough to keep it"
);
assert!(
foreign(&["[\"1.21.11\"]"]),
"the jars the old stash dragged in have to go"
);
assert!(
!foreign(&["[\"26.1\"]"]),
"a build for the release this cluster was migrated from stays"
);
assert!(
foreign(&["[\"1.21.1\"]"]),
"a shorter version that is not a component prefix is still foreign"
);
}
#[test]
fn a_pre_release_tag_is_the_same_game_version() {
let foreign = |rows: &[&str], mc_version: &str| {
built_for_another_game_version(
&rows.iter().map(|r| (*r).to_string()).collect::<Vec<_>>(),
mc_version,
)
};
assert!(
!foreign(&["[\"26.3-rc-1\"]"], "26.3"),
"a jar built against the release candidate runs on the release"
);
assert!(!foreign(&["[\"26.3-snapshot-7\",\"26.3-pre-2\"]"], "26.3"));
assert!(
!foreign(&["[\"26.3\"]"], "26.3-snapshot-10"),
"and a snapshot cluster keeps the release build"
);
assert!(
!foreign(&["[\"26.3-rc-1\"]"], "26.3.1"),
"the component prefix still applies once the tag is off"
);
assert!(
foreign(&["[\"26.2-rc-1\"]"], "26.3"),
"stripping the tag must not blur two game versions together"
);
}
#[test]
fn only_projects_with_several_copies_are_reconciled() {
let groups = group_duplicates(&[

View file

@ -15,7 +15,7 @@ mod file_identity;
pub mod markdown;
// Re-exported so `oneclient_core::packages::ContentType` keeps working
pub use activity::{disable_foreign_game_versions, reconcile_duplicate_activity};
pub use activity::reconcile_duplicate_activity;
pub use bad_mods::{
BadMod, BadModAlternative, BadModList, ProjectIds, ResolvedAlternative, fetch_bad_mods,
fetch_explanation, load_bad_mods, refresh_bad_mods, resolve_alternatives,

View file

@ -168,39 +168,6 @@ pub async fn materialize_content(
tracing::warn!(cluster_id = cluster.id, %err, "failed to resolve duplicate package versions");
}
match oneclient_content::packages::disable_foreign_game_versions(
cluster.id,
&cluster.mc_version,
cluster.mc_loader as i64,
&services.content(),
)
.await
{
Ok(disabled) if !disabled.is_empty() => {
let names = removal_summary(&disabled);
let body = if disabled.len() == 1 {
format!(
"{names} is built for a different Minecraft version, so it has been switched off in {}.",
cluster.name
)
} else {
format!(
"{names} are built for a different Minecraft version, so they have been switched off in {}.",
cluster.name
)
};
services
.events
.notify("Incompatible mods switched off")
.body(body)
.send();
}
Ok(_) => {}
Err(err) => {
tracing::warn!(cluster_id = cluster.id, %err, "failed to switch off mods built for another game version");
}
}
let active_mods_dir = if mods_in_cluster {
cluster_dir.as_path()
} else {

View file

@ -236,19 +236,6 @@ pub async fn get_release_by_hash(
.await
}
pub async fn list_release_game_versions(
pool: &SqlitePool,
hash: &str,
) -> Result<Vec<String>, sqlx::Error> {
let rows: Vec<(String,)> =
sqlx::query_as("SELECT mc_versions FROM provider_releases WHERE hash = ?")
.bind(hash)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|(versions,)| versions).collect())
}
pub async fn link_cluster_artifact(
pool: &SqlitePool,
cluster_id: i64,