1use crate::registry::profiles_v2;
2use crate::types::UnrealSampleRateV2;
3use crate::{
4 AnimationAddressability, BakeOrExtract, ConversionControl, DefaultStatus, EngineProfile,
5 EngineProfileV2, FactState, FactValue, ForwardAxis, Handedness, ImportHandling, LinearUnit,
6 ResolvedClipCoverageV2, ResolvedProfile, ResolvedProfileSettingsV2, ResolvedProfileV2,
7 ResolvedSettingOriginV2, RootMotionAddressability, SettingApplicability, SettingDefaultV2,
8 SettingDomainV2, SettingId, SettingIdV2, SettingScope, SettingValue, SettingValueV2,
9 TargetAddressability, UpAxis,
10};
11use animsmith_core::engine_contract::{
12 EngineAnimationAddressabilityV1, EngineBakeOrExtractV1, EngineClipSettingsV1,
13 EngineContractError, EngineConversionControlV1, EngineCoordinateBasisV1, EngineDefaultStatusV1,
14 EngineFactIdV1, EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
15 EngineImportHandlingV1, EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1,
16 EngineProfileSelectionV1, EngineRootMotionAddressabilityV1, EngineSettingApplicabilityV1,
17 EngineSettingDescriptorV1, EngineSettingDomainV1, EngineSettingIdV1, EngineSettingRowV1,
18 EngineSettingScopeV1, EngineSettingValueV1, EngineTargetAddressabilityV1, EngineUpAxisV1,
19 ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2, ResolvedEngineSettingsV1,
20 ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
21};
22use animsmith_core::engine_contract::{
23 EngineClipSettingsV3 as CoreClipSettingsV3, EnginePrimarySourceV2 as CorePrimarySourceV2,
24 EngineSampleRateV2 as CoreSampleRateV2, EngineSettingDescriptorV2 as CoreSettingDescriptorV2,
25 EngineSettingDomainV2 as CoreSettingDomainV2, EngineSettingIdV2 as CoreSettingIdV2,
26 EngineSettingRowV3, EngineSettingValueOriginV3, EngineSettingValueV2 as CoreSettingValueV2,
27 ResolvedEngineProfileV2 as CoreEngineProfileV2,
28 ResolvedEngineSettingsCoverageV2 as CoreSettingsCoverageV2,
29 ResolvedEngineSettingsV3 as CoreSettingsV3, ResolvedEngineSettingsWorkV2 as CoreSettingsWorkV2,
30};
31use animsmith_core::{InputIdentity, SourceFormatV1};
32use std::collections::BTreeMap;
33
34pub(crate) fn facts_identity(profile: &EngineProfile) -> InputIdentity {
35 project_profile(profile)
36 .expect("the frozen engine registry must project into its core-owned V1 contract")
37 .facts_identity()
38 .clone()
39}
40
41pub(crate) fn matches_frozen_registry_projection_v2(profile: &CoreEngineProfileV2) -> bool {
48 profiles_v2().iter().any(|candidate| {
49 project_engine_profile_v2(candidate).is_ok_and(|projected| {
50 projected.selection() == profile.selection() && &projected == profile
51 })
52 })
53}
54
55pub(crate) fn settings_identity<'a>(
56 profile: &EngineProfile,
57 document: &BTreeMap<SettingId, SettingValue>,
58 clips: impl IntoIterator<Item = (&'a str, &'a BTreeMap<SettingId, SettingValue>)>,
59) -> Result<InputIdentity, EngineContractError> {
60 let profile = project_profile(profile)?;
61 Ok(project_settings(&profile, document, clips)?
62 .settings_identity()
63 .clone())
64}
65
66pub(crate) fn project_resolved_profile(
67 resolved: &ResolvedProfile,
68) -> Result<(ResolvedEngineProfileV1, ResolvedEngineSettingsV1), EngineContractError> {
69 let profile = project_profile(resolved.profile())?;
70 let settings = project_settings(
71 &profile,
72 resolved.document_settings(),
73 resolved
74 .clip_settings()
75 .iter()
76 .map(|clip| (clip.clip_name(), clip.settings())),
77 )?;
78 Ok((profile, settings))
79}
80
81pub(crate) fn project_resolved_profile_v2(
83 resolved: &ResolvedProfileV2,
84) -> Result<(ResolvedEngineProfileV1, ResolvedEngineSettingsV2), EngineContractError> {
85 let profile = project_profile(resolved.profile())?;
86 let coverage = match resolved.clip_coverage() {
87 ResolvedClipCoverageV2::Complete => ResolvedEngineSettingsCoverageV2::complete(),
88 ResolvedClipCoverageV2::Partial { .. } => {
89 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded()
90 }
91 };
92 let work = resolved.work();
93 let settings = ResolvedEngineSettingsV2::new(
94 &profile,
95 resolved
96 .document_settings()
97 .iter()
98 .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
99 .collect(),
100 resolved
101 .clip_settings()
102 .iter()
103 .map(|clip| {
104 EngineClipSettingsV1::new(
105 clip.clip_name(),
106 clip.settings()
107 .iter()
108 .map(|(id, value)| {
109 EngineSettingRowV1::new(setting_id(*id), setting_value(value))
110 })
111 .collect(),
112 )
113 })
114 .collect::<Result<Vec<_>, _>>()?,
115 coverage,
116 ResolvedEngineSettingsWorkV2::new(
117 work.actual_clip_rows_inspected(),
118 work.materialized_clip_rows(),
119 work.retained_clip_rows(),
120 ),
121 )?;
122 Ok((profile, settings))
123}
124
125pub fn project_engine_profile_v2(
128 profile: &EngineProfileV2,
129) -> Result<CoreEngineProfileV2, EngineContractError> {
130 let selection = profile.selection();
131 CoreEngineProfileV2::new(
132 EngineProfileSelectionV1::new(
133 selection.family(),
134 selection.profile_revision(),
135 selection.engine_version(),
136 selection.importer(),
137 )?,
138 profile.profile_urn(),
139 profile.facts().to_vec(),
140 profile
141 .setting_descriptors()
142 .iter()
143 .map(|descriptor| {
144 CoreSettingDescriptorV2::new(
145 setting_id_v2(descriptor.id()),
146 match descriptor.scope() {
147 SettingScope::Document => EngineSettingScopeV1::Document,
148 SettingScope::Clip => EngineSettingScopeV1::Clip,
149 },
150 match descriptor.domain() {
151 SettingDomainV2::Boolean => CoreSettingDomainV2::Boolean,
152 SettingDomainV2::BakeOrExtract => CoreSettingDomainV2::BakeOrExtract,
153 SettingDomainV2::SourceTransformPath => {
154 CoreSettingDomainV2::SourceTransformPath
155 }
156 SettingDomainV2::AnimationType | SettingDomainV2::AvatarSetup => {
157 CoreSettingDomainV2::Token
158 }
159 SettingDomainV2::LoadMeshesState | SettingDomainV2::HandlerEnvironment => {
160 CoreSettingDomainV2::Token
161 }
162 SettingDomainV2::PositiveInteger => CoreSettingDomainV2::PositiveInteger,
163 SettingDomainV2::SampleRate => CoreSettingDomainV2::SampleRate,
164 },
165 profile.accepted_inputs().to_vec(),
166 match descriptor.default() {
167 SettingDefaultV2::RequiredExplicit => None,
168 SettingDefaultV2::Verified(value) => Some(setting_value_v2(value)),
169 },
170 )
171 })
172 .collect::<Result<Vec<_>, _>>()?,
173 profile
174 .sources()
175 .iter()
176 .map(|source| {
177 CorePrimarySourceV2::new(
178 source.id(),
179 source.target_version(),
180 source.url(),
181 source.verified_on(),
182 source.supported_facts().to_vec(),
183 source
184 .supported_settings()
185 .iter()
186 .copied()
187 .map(setting_id_v2)
188 .collect(),
189 )
190 })
191 .collect::<Result<Vec<_>, _>>()?,
192 )
193}
194
195pub fn project_resolved_engine_settings_v3(
198 resolved: &ResolvedProfileSettingsV2,
199) -> Result<(CoreEngineProfileV2, CoreSettingsV3), EngineContractError> {
200 let profile = project_engine_profile_v2(resolved.profile())?;
201 let document_settings = resolved
202 .document_settings()
203 .iter()
204 .map(|(id, resolved)| {
205 EngineSettingRowV3::new(
206 setting_id_v2(*id),
207 setting_value_v2(resolved.value()),
208 match resolved.origin() {
209 ResolvedSettingOriginV2::ExplicitConfig => {
210 EngineSettingValueOriginV3::ExplicitConfig
211 }
212 ResolvedSettingOriginV2::ProfileDefault => {
213 EngineSettingValueOriginV3::ProfileDefault
214 }
215 },
216 )
217 })
218 .collect::<Vec<_>>();
219 let clips = resolved
220 .clip_settings()
221 .iter()
222 .map(|clip| {
223 CoreClipSettingsV3::new(
224 clip.clip_ordinal(),
225 clip.clip_name(),
226 clip.settings()
227 .iter()
228 .map(|(id, resolved)| {
229 EngineSettingRowV3::new(
230 setting_id_v2(*id),
231 setting_value_v2(resolved.value()),
232 match resolved.origin() {
233 ResolvedSettingOriginV2::ExplicitConfig => {
234 EngineSettingValueOriginV3::ExplicitConfig
235 }
236 ResolvedSettingOriginV2::ProfileDefault => {
237 EngineSettingValueOriginV3::ProfileDefault
238 }
239 },
240 )
241 })
242 .collect(),
243 )
244 })
245 .collect::<Result<Vec<_>, _>>()?;
246 let coverage = match resolved.clip_coverage() {
247 ResolvedClipCoverageV2::Complete => CoreSettingsCoverageV2::complete(),
248 ResolvedClipCoverageV2::Partial { .. } => {
249 CoreSettingsCoverageV2::actual_clip_rows_exceeded()
250 }
251 };
252 let work = resolved.work();
253 let settings = CoreSettingsV3::new(
254 &profile,
255 resolved.source_format(),
256 document_settings,
257 clips,
258 coverage,
259 CoreSettingsWorkV2::new(
260 work.actual_clip_rows_inspected(),
261 work.materialized_clip_rows(),
262 work.retained_clip_rows(),
263 ),
264 )?;
265 Ok((profile, settings))
266}
267
268fn project_profile(
269 profile: &EngineProfile,
270) -> Result<ResolvedEngineProfileV1, EngineContractError> {
271 let selection = profile.selection();
272 ResolvedEngineProfileV1::new(
273 EngineProfileSelectionV1::new(
274 selection.family(),
275 selection.profile_revision(),
276 selection.engine_version(),
277 selection.importer(),
278 )?,
279 profile.fact_bundle_urn(),
280 profile
281 .facts()
282 .iter()
283 .map(|fact| EngineProfileFactV1::new(fact_id(fact.id()), fact_state(fact.state())))
284 .collect(),
285 profile
286 .setting_descriptors()
287 .iter()
288 .map(|descriptor| {
289 EngineSettingDescriptorV1::new(
290 setting_id(descriptor.id()),
291 match descriptor.scope() {
292 SettingScope::Document => EngineSettingScopeV1::Document,
293 SettingScope::Clip => EngineSettingScopeV1::Clip,
294 },
295 match descriptor.domain() {
296 crate::SettingDomain::Boolean => EngineSettingDomainV1::Boolean,
297 crate::SettingDomain::BakeOrExtract => EngineSettingDomainV1::BakeOrExtract,
298 crate::SettingDomain::SourceTransformPath => {
299 EngineSettingDomainV1::SourceTransformPath
300 }
301 },
302 match descriptor.applicability() {
303 SettingApplicability::Applicable => {
304 EngineSettingApplicabilityV1::Applicable
305 }
306 SettingApplicability::NotApplicable => {
307 EngineSettingApplicabilityV1::NotApplicable
308 }
309 },
310 match descriptor.default_status() {
311 DefaultStatus::RequiredWithoutDefault => {
312 EngineDefaultStatusV1::RequiredWithoutDefault
313 }
314 DefaultStatus::NotApplicable => EngineDefaultStatusV1::NotApplicable,
315 },
316 )
317 })
318 .collect(),
319 profile
320 .sources()
321 .iter()
322 .map(|source| {
323 EnginePrimarySourceV1::new(
324 source.id(),
325 source.target_version(),
326 source.url(),
327 source.verified_on(),
328 source
329 .supported_facts()
330 .iter()
331 .copied()
332 .map(fact_id)
333 .collect(),
334 source
335 .supported_settings()
336 .iter()
337 .copied()
338 .map(setting_id)
339 .collect(),
340 )
341 })
342 .collect::<Result<Vec<_>, _>>()?,
343 )
344}
345
346fn project_settings<'a>(
347 profile: &ResolvedEngineProfileV1,
348 document: &BTreeMap<SettingId, SettingValue>,
349 clips: impl IntoIterator<Item = (&'a str, &'a BTreeMap<SettingId, SettingValue>)>,
350) -> Result<ResolvedEngineSettingsV1, EngineContractError> {
351 ResolvedEngineSettingsV1::new(
352 profile,
353 document
354 .iter()
355 .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
356 .collect(),
357 clips
358 .into_iter()
359 .map(|(clip_name, values)| {
360 EngineClipSettingsV1::new(
361 clip_name,
362 values
363 .iter()
364 .map(|(id, value)| {
365 EngineSettingRowV1::new(setting_id(*id), setting_value(value))
366 })
367 .collect(),
368 )
369 })
370 .collect::<Result<Vec<_>, _>>()?,
371 )
372}
373
374pub(crate) fn validate_clip_settings(
376 clip_name: &str,
377 values: &BTreeMap<SettingId, SettingValue>,
378) -> Result<(), EngineContractError> {
379 EngineClipSettingsV1::new(
380 clip_name,
381 values
382 .iter()
383 .map(|(id, value)| EngineSettingRowV1::new(setting_id(*id), setting_value(value)))
384 .collect(),
385 )?;
386 Ok(())
387}
388
389fn fact_state(state: &FactState) -> EngineFactStateV1 {
390 match state {
391 FactState::Known(value) => EngineFactStateV1::Known(fact_value(value)),
392 FactState::Unknown => EngineFactStateV1::Unknown,
393 FactState::NotApplicable => EngineFactStateV1::NotApplicable,
394 }
395}
396
397fn fact_value(value: &FactValue) -> EngineFactValueV1 {
398 match value {
399 FactValue::AcceptedFormats(formats) => EngineFactValueV1::AcceptedFormats(formats.clone()),
400 FactValue::AnimationAddressability(AnimationAddressability::GltfAssetLabel) => {
401 EngineFactValueV1::AnimationAddressability(
402 EngineAnimationAddressabilityV1::GltfAssetLabel,
403 )
404 }
405 FactValue::CoordinateBasis(value) => {
406 EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
407 handedness: match value.handedness {
408 Handedness::Left => EngineHandednessV1::Left,
409 Handedness::Right => EngineHandednessV1::Right,
410 },
411 up_axis: match value.up_axis {
412 UpAxis::X => EngineUpAxisV1::X,
413 UpAxis::Y => EngineUpAxisV1::Y,
414 UpAxis::Z => EngineUpAxisV1::Z,
415 },
416 forward_axis: match value.forward_axis {
417 ForwardAxis::PositiveX => EngineForwardAxisV1::PositiveX,
418 ForwardAxis::NegativeX => EngineForwardAxisV1::NegativeX,
419 ForwardAxis::PositiveY => EngineForwardAxisV1::PositiveY,
420 ForwardAxis::NegativeY => EngineForwardAxisV1::NegativeY,
421 ForwardAxis::PositiveZ => EngineForwardAxisV1::PositiveZ,
422 ForwardAxis::NegativeZ => EngineForwardAxisV1::NegativeZ,
423 },
424 })
425 }
426 FactValue::LinearUnit(value) => EngineFactValueV1::LinearUnit(match value {
427 LinearUnit::Metre => EngineLinearUnitV1::Metre,
428 LinearUnit::Centimetre => EngineLinearUnitV1::Centimetre,
429 }),
430 FactValue::ConversionControl(value) => EngineFactValueV1::ConversionControl(match value {
431 ConversionControl::ProfileSetting(id) => {
432 EngineConversionControlV1::ProfileSetting(setting_id(*id))
433 }
434 ConversionControl::ImporterOption => EngineConversionControlV1::ImporterOption,
435 }),
436 FactValue::Boolean(value) => EngineFactValueV1::Boolean(*value),
437 FactValue::ImportHandling(value) => EngineFactValueV1::ImportHandling(match value {
438 ImportHandling::Preserved => EngineImportHandlingV1::Preserved,
439 ImportHandling::Converted => EngineImportHandlingV1::Converted,
440 ImportHandling::Discarded => EngineImportHandlingV1::Discarded,
441 ImportHandling::Unsupported => EngineImportHandlingV1::Unsupported,
442 }),
443 FactValue::TargetAddressability(TargetAddressability::NamePathDerivedId) => {
444 EngineFactValueV1::TargetAddressability(EngineTargetAddressabilityV1::NamePathDerivedId)
445 }
446 FactValue::RootMotionAddressability(value) => {
447 EngineFactValueV1::RootMotionAddressability(match value {
448 RootMotionAddressability::ExactSourceTransformPath => {
449 EngineRootMotionAddressabilityV1::ExactSourceTransformPath
450 }
451 RootMotionAddressability::HumanoidAvatarBody => {
452 EngineRootMotionAddressabilityV1::HumanoidAvatarBody
453 }
454 })
455 }
456 }
457}
458
459fn setting_value(value: &SettingValue) -> EngineSettingValueV1 {
460 match value {
461 SettingValue::Boolean(value) => EngineSettingValueV1::Boolean(*value),
462 SettingValue::BakeOrExtract(value) => EngineSettingValueV1::BakeOrExtract(match value {
463 BakeOrExtract::Bake => EngineBakeOrExtractV1::Bake,
464 BakeOrExtract::Extract => EngineBakeOrExtractV1::Extract,
465 }),
466 SettingValue::SourceTransformPath(value) => {
467 EngineSettingValueV1::SourceTransformPath(value.clone())
468 }
469 }
470}
471
472const fn fact_id(id: crate::FactId) -> EngineFactIdV1 {
473 match id {
474 crate::FactId::AcceptedInputs => EngineFactIdV1::AcceptedInputs,
475 crate::FactId::AnimationAddressability => EngineFactIdV1::AnimationAddressability,
476 crate::FactId::TargetCoordinateBasis => EngineFactIdV1::TargetCoordinateBasis,
477 crate::FactId::TargetLinearUnit => EngineFactIdV1::TargetLinearUnit,
478 crate::FactId::UnitConversionControl => EngineFactIdV1::UnitConversionControl,
479 crate::FactId::AxisConversionControl => EngineFactIdV1::AxisConversionControl,
480 crate::FactId::ExactAxisConversion => EngineFactIdV1::ExactAxisConversion,
481 crate::FactId::ResultingHierarchyScale => EngineFactIdV1::ResultingHierarchyScale,
482 crate::FactId::WholeEndFrameRequired => EngineFactIdV1::WholeEndFrameRequired,
483 crate::FactId::AnimationChannelHandling => EngineFactIdV1::AnimationChannelHandling,
484 crate::FactId::ExtensionHandling => EngineFactIdV1::ExtensionHandling,
485 crate::FactId::ConstructHandling => EngineFactIdV1::ConstructHandling,
486 crate::FactId::AnimationTargetAddressability => {
487 EngineFactIdV1::AnimationTargetAddressability
488 }
489 crate::FactId::RootMotionAddressability => EngineFactIdV1::RootMotionAddressability,
490 }
491}
492
493const fn setting_id(id: SettingId) -> EngineSettingIdV1 {
494 match id {
495 SettingId::ConvertUnits => EngineSettingIdV1::ConvertUnits,
496 SettingId::BakeAxisConversion => EngineSettingIdV1::BakeAxisConversion,
497 SettingId::RootMotionSource => EngineSettingIdV1::RootMotionSource,
498 SettingId::RootRotation => EngineSettingIdV1::RootRotation,
499 SettingId::RootPositionY => EngineSettingIdV1::RootPositionY,
500 SettingId::RootPositionXz => EngineSettingIdV1::RootPositionXz,
501 }
502}
503
504const fn setting_id_v2(id: SettingIdV2) -> CoreSettingIdV2 {
505 match id {
506 SettingIdV2::AnimationType => CoreSettingIdV2::AnimationType,
507 SettingIdV2::AvatarSetup => CoreSettingIdV2::AvatarSetup,
508 SettingIdV2::ImportAnimation => CoreSettingIdV2::ImportAnimation,
509 SettingIdV2::RootMotionSource => CoreSettingIdV2::RootMotionSource,
510 SettingIdV2::RootRotation => CoreSettingIdV2::RootRotation,
511 SettingIdV2::RootPositionY => CoreSettingIdV2::RootPositionY,
512 SettingIdV2::RootPositionXz => CoreSettingIdV2::RootPositionXz,
513 SettingIdV2::RotateSceneEntity => CoreSettingIdV2::RotateSceneEntity,
514 SettingIdV2::RotateMeshes => CoreSettingIdV2::RotateMeshes,
515 SettingIdV2::LoadMeshes => CoreSettingIdV2::LoadMeshes,
516 SettingIdV2::ExtensionHandlerEnvironment => CoreSettingIdV2::ExtensionHandlerEnvironment,
517 SettingIdV2::BevyAnimationFeature => CoreSettingIdV2::BevyAnimationFeature,
518 SettingIdV2::LoadAnimations => CoreSettingIdV2::LoadAnimations,
519 SettingIdV2::AnimationFps => CoreSettingIdV2::AnimationFps,
520 SettingIdV2::AnimationTrimming => CoreSettingIdV2::AnimationTrimming,
521 SettingIdV2::SampleRate => CoreSettingIdV2::SampleRate,
522 }
523}
524
525fn setting_value_v2(value: &SettingValueV2) -> CoreSettingValueV2 {
526 match value {
527 SettingValueV2::Boolean(value) => CoreSettingValueV2::Boolean(*value),
528 SettingValueV2::BakeOrExtract(value) => CoreSettingValueV2::BakeOrExtract(match value {
529 crate::BakeOrExtract::Bake => {
530 animsmith_core::engine_contract::EngineBakeOrExtractV1::Bake
531 }
532 crate::BakeOrExtract::Extract => {
533 animsmith_core::engine_contract::EngineBakeOrExtractV1::Extract
534 }
535 }),
536 SettingValueV2::SourceTransformPath(value) => {
537 CoreSettingValueV2::SourceTransformPath(value.clone())
538 }
539 SettingValueV2::AnimationType(value) => CoreSettingValueV2::Token(value.as_str().into()),
540 SettingValueV2::AvatarSetup(value) => CoreSettingValueV2::Token(value.as_str().into()),
541 SettingValueV2::LoadMeshesState(value) => CoreSettingValueV2::Token(
542 match value {
543 crate::BevyLoadMeshesStateV2::Empty => "empty",
544 crate::BevyLoadMeshesStateV2::Nonempty => "nonempty",
545 }
546 .into(),
547 ),
548 SettingValueV2::HandlerEnvironment(value) => {
549 CoreSettingValueV2::Token(value.as_str().into())
550 }
551 SettingValueV2::PositiveInteger(value) => CoreSettingValueV2::PositiveInteger(*value),
552 SettingValueV2::SampleRate(value) => CoreSettingValueV2::SampleRate(match value {
553 UnrealSampleRateV2::Default30 => CoreSampleRateV2::Default30,
554 UnrealSampleRateV2::SourceDetermined => CoreSampleRateV2::SourceDetermined,
555 UnrealSampleRateV2::CustomHz(value) => CoreSampleRateV2::CustomHz(*value),
556 }),
557 }
558}
559
560pub(crate) const fn format_name(format: SourceFormatV1) -> &'static str {
561 match format {
562 SourceFormatV1::GltfJson => "gltf_json",
563 SourceFormatV1::Glb => "glb",
564 SourceFormatV1::Fbx => "fbx",
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::{
572 FactId, ProfileFact, SettingDescriptor, SettingDomainV2, SettingValueV2,
573 types::UnrealSampleRateV2,
574 };
575
576 #[test]
577 fn advice_setting_values_map_to_the_closed_core_vocabulary_and_bounds() {
578 assert_eq!(
579 setting_value_v2(&SettingValueV2::PositiveInteger(120)),
580 CoreSettingValueV2::PositiveInteger(120)
581 );
582 assert!(
583 !SettingValueV2::PositiveInteger(0).matches_domain(SettingDomainV2::PositiveInteger)
584 );
585 assert!(
586 !SettingValueV2::PositiveInteger(121).matches_domain(SettingDomainV2::PositiveInteger)
587 );
588 for (public, core) in [
589 (UnrealSampleRateV2::Default30, CoreSampleRateV2::Default30),
590 (
591 UnrealSampleRateV2::SourceDetermined,
592 CoreSampleRateV2::SourceDetermined,
593 ),
594 (
595 UnrealSampleRateV2::CustomHz(48_000),
596 CoreSampleRateV2::CustomHz(48_000),
597 ),
598 ] {
599 assert!(SettingValueV2::SampleRate(public).matches_domain(SettingDomainV2::SampleRate));
600 assert_eq!(
601 setting_value_v2(&SettingValueV2::SampleRate(public)),
602 CoreSettingValueV2::SampleRate(core)
603 );
604 }
605 for invalid in [0, 48_001] {
606 assert!(
607 !SettingValueV2::SampleRate(UnrealSampleRateV2::CustomHz(invalid))
608 .matches_domain(SettingDomainV2::SampleRate)
609 );
610 }
611 }
612
613 #[test]
614 fn facts_encoding_sorts_every_repeated_stable_id() {
615 for original in crate::profiles_v1() {
616 let expected = facts_identity(original);
617 let mut reordered = original.clone();
618 let accepted = reordered
619 .facts
620 .iter_mut()
621 .find(|fact| fact.id() == FactId::AcceptedInputs)
622 .unwrap();
623 if let FactState::Known(FactValue::AcceptedFormats(formats)) = accepted.state() {
624 let mut formats = formats.clone();
625 formats.reverse();
626 *accepted = ProfileFact::new(
627 FactId::AcceptedInputs,
628 FactState::Known(FactValue::AcceptedFormats(formats)),
629 );
630 }
631 reordered.facts.reverse();
632 reordered.settings.reverse();
633 reordered.sources.reverse();
634 for source in &mut reordered.sources {
635 source.reverse_supported_ids_for_test();
636 }
637 assert_eq!(
638 facts_identity(&reordered),
639 expected,
640 "{:?}",
641 original.selection()
642 );
643 }
644 }
645
646 #[test]
647 fn facts_encoding_is_sensitive_to_each_record_domain() {
648 let original = crate::profiles_v1()
649 .iter()
650 .find(|profile| profile.selection.family() == "unity-generic")
651 .unwrap()
652 .clone();
653 let expected = facts_identity(&original);
654 let changed = |profile: &EngineProfile| {
655 if let Ok(projected) = project_profile(profile) {
656 assert_ne!(projected.facts_identity(), &expected);
657 }
658 };
659
660 let mut profile = original.clone();
661 profile.selection = crate::ProfileSelection::new(
662 "unity-generic-changed",
663 1,
664 "6000.3",
665 "fbx-model-importer",
666 );
667 changed(&profile);
668
669 let mut profile = original.clone();
670 profile.selection =
671 crate::ProfileSelection::new("unity-generic", 2, "6000.3", "fbx-model-importer");
672 changed(&profile);
673
674 let mut profile = original.clone();
675 profile.selection =
676 crate::ProfileSelection::new("unity-generic", 1, "6000.4", "fbx-model-importer");
677 changed(&profile);
678
679 let mut profile = original.clone();
680 profile.selection =
681 crate::ProfileSelection::new("unity-generic", 1, "6000.3", "other-importer");
682 changed(&profile);
683
684 let mut profile = original.clone();
685 profile.fact_bundle_urn = "urn:animsmith:engine-profile:unity-generic:changed";
686 changed(&profile);
687
688 let mut profile = original.clone();
689 let fact = profile
690 .facts
691 .iter_mut()
692 .find(|fact| fact.id() == FactId::ExactAxisConversion)
693 .unwrap();
694 *fact = ProfileFact::new(FactId::ExactAxisConversion, FactState::NotApplicable);
695 changed(&profile);
696
697 let mut profile = original.clone();
698 let fact = profile
699 .facts
700 .iter_mut()
701 .find(|fact| fact.id() == FactId::AcceptedInputs)
702 .unwrap();
703 *fact = ProfileFact::new(
704 FactId::AcceptedInputs,
705 FactState::Known(FactValue::AcceptedFormats(vec![
706 SourceFormatV1::Fbx,
707 SourceFormatV1::Glb,
708 ])),
709 );
710 changed(&profile);
711
712 let mut profile = original.clone();
713 let descriptor = profile.settings[0].clone();
714 profile.settings[0] = SettingDescriptor::new(
715 descriptor.id(),
716 descriptor.scope(),
717 crate::SettingDomain::BakeOrExtract,
718 descriptor.applicability(),
719 descriptor.default_status(),
720 );
721 changed(&profile);
722
723 let mut profile = original.clone();
724 let descriptor = profile.settings[0].clone();
725 profile.settings[0] = SettingDescriptor::new(
726 descriptor.id(),
727 crate::SettingScope::Clip,
728 descriptor.domain(),
729 descriptor.applicability(),
730 descriptor.default_status(),
731 );
732 changed(&profile);
733
734 let mut profile = original.clone();
735 let descriptor = profile.settings[0].clone();
736 profile.settings[0] = SettingDescriptor::new(
737 descriptor.id(),
738 descriptor.scope(),
739 descriptor.domain(),
740 crate::SettingApplicability::NotApplicable,
741 descriptor.default_status(),
742 );
743 changed(&profile);
744
745 let mut profile = original.clone();
746 let descriptor = profile.settings[0].clone();
747 profile.settings[0] = SettingDescriptor::new(
748 descriptor.id(),
749 descriptor.scope(),
750 descriptor.domain(),
751 descriptor.applicability(),
752 crate::DefaultStatus::NotApplicable,
753 );
754 changed(&profile);
755
756 let mut profile = original.clone();
757 let source = profile.sources[0].clone();
758 profile.sources[0] = crate::PrimarySource::new(
759 "changed-source-id",
760 source.target_version(),
761 source.url(),
762 source.verified_on(),
763 source.supported_facts().to_vec(),
764 source.supported_settings().to_vec(),
765 );
766 changed(&profile);
767
768 let replace_source = |profile: &mut EngineProfile,
769 target_version,
770 url,
771 verified_on,
772 facts: Vec<_>,
773 settings: Vec<_>| {
774 let source = profile.sources[0].clone();
775 profile.sources[0] = crate::PrimarySource::new(
776 source.id(),
777 target_version,
778 url,
779 verified_on,
780 facts,
781 settings,
782 );
783 };
784
785 let mut profile = original.clone();
786 let source = profile.sources[0].clone();
787 replace_source(
788 &mut profile,
789 "changed-version",
790 source.url(),
791 source.verified_on(),
792 source.supported_facts().to_vec(),
793 source.supported_settings().to_vec(),
794 );
795 changed(&profile);
796
797 let mut profile = original.clone();
798 let source = profile.sources[0].clone();
799 replace_source(
800 &mut profile,
801 source.target_version(),
802 "https://example.invalid/changed",
803 source.verified_on(),
804 source.supported_facts().to_vec(),
805 source.supported_settings().to_vec(),
806 );
807 changed(&profile);
808
809 let mut profile = original.clone();
810 let source = profile.sources[0].clone();
811 replace_source(
812 &mut profile,
813 source.target_version(),
814 source.url(),
815 "2099-01-01",
816 source.supported_facts().to_vec(),
817 source.supported_settings().to_vec(),
818 );
819 changed(&profile);
820
821 let mut profile = original.clone();
822 let source = profile.sources[0].clone();
823 let mut facts = source.supported_facts().to_vec();
824 facts.push(FactId::AcceptedInputs);
825 replace_source(
826 &mut profile,
827 source.target_version(),
828 source.url(),
829 source.verified_on(),
830 facts,
831 source.supported_settings().to_vec(),
832 );
833 changed(&profile);
834
835 let mut profile = original.clone();
836 let source = profile.sources[0].clone();
837 let mut settings = source.supported_settings().to_vec();
838 settings.pop();
839 replace_source(
840 &mut profile,
841 source.target_version(),
842 source.url(),
843 source.verified_on(),
844 source.supported_facts().to_vec(),
845 settings,
846 );
847 changed(&profile);
848
849 let unreal = crate::profiles_v1()
850 .iter()
851 .find(|profile| profile.selection.family() == "unreal")
852 .unwrap();
853 let expected = facts_identity(unreal);
854 let mut profile = unreal.clone();
855 let fact = profile
856 .facts
857 .iter_mut()
858 .find(|fact| fact.id() == FactId::TargetCoordinateBasis)
859 .unwrap();
860 *fact = ProfileFact::new(
861 FactId::TargetCoordinateBasis,
862 FactState::Known(FactValue::CoordinateBasis(crate::CoordinateBasis {
863 handedness: crate::Handedness::Left,
864 up_axis: crate::UpAxis::Z,
865 forward_axis: crate::ForwardAxis::NegativeX,
866 })),
867 );
868 assert_ne!(facts_identity(&profile), expected);
869 }
870
871 #[test]
872 fn frozen_registry_projection_requires_full_profile_equality() {
873 let registry_profile = crate::profiles_v2()
874 .iter()
875 .find(|profile| {
876 let selection = profile.selection();
877 selection.family() == "bevy"
878 && selection.profile_revision() == 2
879 && selection.engine_version() == "0.19.0"
880 && selection.importer() == "gltf-asset-loader"
881 })
882 .expect("frozen Bevy revision-2 profile");
883 let projected = project_engine_profile_v2(registry_profile).unwrap();
884 assert!(matches_frozen_registry_projection_v2(&projected));
885
886 let mut altered = registry_profile.clone();
887 altered.profile_urn = "urn:animsmith:engine-profile:bevy:revision-2-altered";
888 let altered_projection = project_engine_profile_v2(&altered).unwrap();
889 assert_ne!(altered_projection, projected);
890 assert!(
891 !matches_frozen_registry_projection_v2(&altered_projection),
892 "a same-tuple profile mutation must not satisfy the registry proof"
893 );
894 }
895}