diff --git a/crates/coa-core/examples/portable_smoke.rs b/crates/coa-core/examples/portable_smoke.rs index 73a4ad2..f4fc5a9 100644 --- a/crates/coa-core/examples/portable_smoke.rs +++ b/crates/coa-core/examples/portable_smoke.rs @@ -56,7 +56,14 @@ fn with_profile(db: &Db, store: &mut Store, base: &ImportOptions, data_dir: &str } let registry = coa_core::portable::extension::ExtensionRegistry::new(); let mut ra = ra_port.and_then(|port| std::env::var("COA_RA_PASSWORD").ok().and_then(|pw| coa_core::ra::Ra::connect_to(port, ra_user, &pw).ok())); - let caps = realm::profile::probe_capabilities(db, Some(std::path::Path::new(data_dir)), ra.as_mut(), ®istry).map_err(|e| e.to_string())?; + let mut caps = realm::profile::probe_capabilities(db, Some(std::path::Path::new(data_dir)), ra.as_mut(), ®istry).map_err(|e| e.to_string())?; + if ra.is_none() { + let remembered = store.realm_profile(server).map_err(|e| e.to_string())?; + caps = realm::profile::with_remembered_progression(caps, remembered.as_ref()).map_err(|e| e.to_string())?; + if caps.progression.is_none() { + eprintln!("realm {server}: its progression (level cap) has never been read from a running core: start it once and run `capabilities` with --ra-port"); + } + } let change = store.set_realm_profile(server, &caps, if ra.is_some() { "live" } else { "offline" }).map_err(|e| e.to_string())?; eprintln!("realm {server}: content profile {}{}", &caps.content_profile_hash[..16], if change.changed() { " (new or changed)" } else { "" }); Ok(ImportOptions { capabilities: Some(std::sync::Arc::new(caps)), extensions: Some(std::sync::Arc::new(registry)), ..base.clone() }) @@ -268,7 +275,7 @@ fn run() -> Result<(), String> { _ if store.server_mappings(model.character_id).map_err(|e| e.to_string())?.iter().any(|m| &m.server_id == server) => coa_core::portable::compat::Operation::Update, _ => coa_core::portable::compat::Operation::OfflineImport, }; - let report = coa_core::portable::compat::evaluate(&coa_core::portable::compat::Inputs { operation, model: &model, capabilities: &caps, knowledge: opts.knowledge.as_deref(), collections: &[], extensions: opts.extensions.as_deref(), projection_decider: opts.projection.is_some() || matches!(operation, coa_core::portable::compat::Operation::OnlineImport) }); + let report = coa_core::portable::compat::evaluate(&coa_core::portable::compat::Inputs { operation, model: &model, capabilities: &caps, knowledge: opts.knowledge.as_deref(), collections: &[], extensions: opts.extensions.as_deref(), projection_decider: opts.projection.is_some() || matches!(operation, coa_core::portable::compat::Operation::OnlineImport | coa_core::portable::compat::Operation::RuntimeSession) && ra_port.is_some() }); println!("{} on {server} (content profile {}): {:?}", report.operation, &caps.content_profile_hash[..16], report.verdict()); for o in &report.outcomes { println!(" {o}"); diff --git a/crates/coa-core/src/portable/realm/profile.rs b/crates/coa-core/src/portable/realm/profile.rs index 78d5ddc..e1b3a93 100644 --- a/crates/coa-core/src/portable/realm/profile.rs +++ b/crates/coa-core/src/portable/realm/profile.rs @@ -96,6 +96,21 @@ pub fn probe_capabilities(db: &Db, data_dir: Option<&Path>, ra: Option<&mut Ra>, assemble(db, &schema, data_dir, report.as_ref(), registry) } +/// A profile assembled without the realm's core (a stopped realm) knows nothing of its progression. The progression the realm's core last +/// reported live (remembered in the store) is the best there is: the profile gets it, and the core still refuses a character prepared for +/// another progression at login, so a realm whose rules changed since is caught, not trusted. +pub fn with_remembered_progression(offline: RealmCapabilities, remembered: Option<&RealmCapabilities>) -> Result { + let Some(progression) = remembered.and_then(|r| r.progression.clone()) else { return Ok(offline) }; + if offline.progression.is_some() { + return Ok(offline); + } + let mut content = offline.content.clone(); + if remembered.is_some_and(|r| r.content.supports(Feature::LevelProjection)) { + content.features.insert(Feature::LevelProjection); + } + RealmCapabilities::build(offline.core.clone(), content, Some(progression)) +} + /// The table-level view an adapter needs to say whether its module is present, before any character is involved. struct NoRealm<'a> { probe: &'a SchemaProbe, diff --git a/crates/coa-core/src/portable/realm/project.rs b/crates/coa-core/src/portable/realm/project.rs index aa6bacc..8a4a8d7 100644 --- a/crates/coa-core/src/portable/realm/project.rs +++ b/crates/coa-core/src/portable/realm/project.rs @@ -234,6 +234,22 @@ mod tests { assert!(matches!(plan_with(&c, 3, Some(&unknown), None), Err(PortableError::ProgressionChanged(_)))); } + #[test] + fn a_stopped_realm_keeps_the_progression_its_core_last_reported() { + use super::super::profile::with_remembered_progression; + let live = caps(60, SIG); + let mut offline = live.clone(); + offline.progression = None; + offline.content.features.remove(&Feature::LevelProjection); + let offline = RealmCapabilities::build(None, offline.content, None).unwrap(); + assert!(offline.needs_reprobe()); + let merged = with_remembered_progression(offline.clone(), Some(&live)).unwrap(); + assert_eq!(merged.progression, live.progression); + assert!(merged.content.supports(Feature::LevelProjection)); + assert!(with_remembered_progression(offline.clone(), None).unwrap().needs_reprobe(), "nothing remembered, nothing invented"); + assert_eq!(with_remembered_progression(live.clone(), Some(&caps(70, &"ab".repeat(32)))).unwrap(), live, "a live profile keeps its own"); + } + #[test] fn a_query_job_names_its_character_and_its_hash() { let c = geared_level_eighty();