aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-assets
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-assets')
-rw-r--r--crates/fparkan-assets/Cargo.toml1
-rw-r--r--crates/fparkan-assets/src/lib.rs571
2 files changed, 572 insertions, 0 deletions
diff --git a/crates/fparkan-assets/Cargo.toml b/crates/fparkan-assets/Cargo.toml
index ce3cb61..c2b8441 100644
--- a/crates/fparkan-assets/Cargo.toml
+++ b/crates/fparkan-assets/Cargo.toml
@@ -6,6 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
+fparkan-binary = { path = "../fparkan-binary" }
fparkan-material = { path = "../fparkan-material" }
fparkan-msh = { path = "../fparkan-msh" }
fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index d34effb..e1abea2 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
//! Asset manager ports and transactional preparation models.
+use fparkan_binary::{checked_count_bytes, read_lp_bytes, Cursor, DecodeError};
use fparkan_material::{
decode_mat0, decode_wear, resolve_material, MaterialFallback, ResolvedMaterial, WearTable,
MAT0_KIND, WEAR_KIND,
@@ -62,6 +63,150 @@ pub struct PreparedMaterialPhase {
pub page_uv_transform: [f32; 4],
}
+/// The two Behavior profile values currently used by recovered resource queries.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct BehaviorProfileRates {
+ /// Second raw value blob for `Transfer_Ore_OffBoard`.
+ pub transfer_ore_off_board_bits: u32,
+ /// Second raw value blob for `Transfer_Power_Out`.
+ pub transfer_power_out_bits: u32,
+}
+
+/// Error while reading a bounded binary Behavior `.var` profile.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum BehaviorProfileVarError {
+ /// The binary record framing is malformed.
+ Decode(DecodeError),
+ /// A bound for one of the two fields used here is not a 32-bit float.
+ InvalidFloatField {
+ name: &'static str,
+ size: u32,
+ type_index: u32,
+ },
+ /// A registered runtime field was absent from the profile.
+ MissingField { name: &'static str },
+}
+
+impl fmt::Display for BehaviorProfileVarError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Decode(source) => write!(f, "{source}"),
+ Self::InvalidFloatField {
+ name,
+ size,
+ type_index,
+ } => write!(
+ f,
+ "field {name} has size {size} and type index {type_index}, expected a 4-byte float"
+ ),
+ Self::MissingField { name } => write!(f, "required field {name} is missing"),
+ }
+ }
+}
+
+impl std::error::Error for BehaviorProfileVarError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::Decode(source) => Some(source),
+ Self::InvalidFloatField { .. } | Self::MissingField { .. } => None,
+ }
+ }
+}
+
+impl From<DecodeError> for BehaviorProfileVarError {
+ fn from(value: DecodeError) -> Self {
+ Self::Decode(value)
+ }
+}
+
+/// Reads the two Behavior rate fields from a native binary `.var` profile.
+///
+/// Native records contain two equally sized value blobs. After binding a
+/// registered destination, the original applies the second blob. Field names
+/// are matched case-sensitively as C strings (up to the first NUL), and the
+/// first occurrence wins. Other fields are consumed but their meanings remain
+/// outside this decoder.
+///
+/// # Errors
+///
+/// Returns [`BehaviorProfileVarError`] when record framing is truncated,
+/// contains trailing bytes, omits a registered field, or declares a selected
+/// field that is not a float row.
+pub fn decode_behavior_profile_rates(
+ bytes: &[u8],
+) -> Result<BehaviorProfileRates, BehaviorProfileVarError> {
+ let mut cursor = Cursor::new(bytes);
+ let count = cursor.read_u32_le()?;
+ checked_count_bytes(u64::from(count), 28, cursor.remaining() as u64)?;
+
+ let mut transfer_ore_off_board_bits = None;
+ let mut transfer_power_out_bits = None;
+ for _ in 0..count {
+ let size = cursor.read_u32_le()?;
+ let type_index = cursor.read_u32_le()?;
+ let _flags = cursor.read_u32_le()?;
+ let field_name = read_lp_bytes(&mut cursor, 64 * 1024)?;
+ let c_string_name = field_name
+ .split(|byte| *byte == 0)
+ .next()
+ .unwrap_or_default();
+ let target_name = match c_string_name {
+ b"Transfer_Ore_OffBoard" => Some("Transfer_Ore_OffBoard"),
+ b"Transfer_Power_Out" => Some("Transfer_Power_Out"),
+ _ => None,
+ };
+ let _type_name = read_lp_bytes(&mut cursor, 64 * 1024)?;
+ let value_size = usize::try_from(size).map_err(|_| DecodeError::IntegerOverflow)?;
+ cursor.read_exact(value_size)?;
+ let capture_target = target_name.is_some_and(|name| {
+ if name == "Transfer_Ore_OffBoard" {
+ transfer_ore_off_board_bits.is_none()
+ } else {
+ transfer_power_out_bits.is_none()
+ }
+ });
+ let second_value_bits = if capture_target {
+ let name = target_name.expect("capture requires a target field");
+ if size != 4 || type_index != 3 {
+ return Err(BehaviorProfileVarError::InvalidFloatField {
+ name,
+ size,
+ type_index,
+ });
+ }
+ let value = cursor.read_exact(value_size)?;
+ Some(u32::from_le_bytes(
+ value.try_into().expect("validated four-byte float row"),
+ ))
+ } else {
+ cursor.read_exact(value_size)?;
+ None
+ };
+ cursor.read_exact(8)?;
+
+ if let (Some(name), Some(value_bits)) = (target_name, second_value_bits) {
+ if name == "Transfer_Ore_OffBoard" {
+ transfer_ore_off_board_bits = Some(value_bits);
+ } else {
+ transfer_power_out_bits = Some(value_bits);
+ }
+ }
+ }
+ cursor.require_eof()?;
+ Ok(BehaviorProfileRates {
+ transfer_ore_off_board_bits: transfer_ore_off_board_bits.ok_or(
+ BehaviorProfileVarError::MissingField {
+ name: "Transfer_Ore_OffBoard",
+ },
+ )?,
+ transfer_power_out_bits: transfer_power_out_bits.ok_or(
+ BehaviorProfileVarError::MissingField {
+ name: "Transfer_Power_Out",
+ },
+ )?,
+ })
+}
+
type WearValidationCache =
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
@@ -1180,6 +1325,19 @@ impl Default for ControlTag21InstanceState {
/// Input or metadata prevented a safe native control sample.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ControlSamplingError {
+ /// This resource-device helper only implements typed-record tag 26.
+ UnsupportedResourceDeviceTag { actual: u32 },
+ /// The record's raw provider key has no resolved NDP table.
+ MissingResourceDeviceNodeData { key: u32 },
+ /// The record's raw provider key is not a row in its NDP table.
+ ResourceDeviceNodeDataKeyOutOfRange { key: u32, row_count: usize },
+ /// The caller did not supply the provider's current type-1 value.
+ MissingResourceDeviceProviderValue { key: u32 },
+ /// A coefficient is outside the helper's finite normal-or-zero contract.
+ InvalidResourceDeviceOperand { operand: &'static str },
+ /// The native x87 intermediate is outside the range represented by normal
+ /// f32 values, so the Rust result could differ from the original.
+ UnsupportedResourceDeviceFloatRange { stage: &'static str },
/// A state object was reused with a different model node count.
StateNodeCountMismatch { expected: usize, actual: usize },
/// A state object was reused with a different number of tag-21 records.
@@ -1216,6 +1374,74 @@ pub enum ControlSamplingError {
NonFiniteSample { record_index: usize },
}
+/// Samples Control's tag-26 typed-record query `0x300` for one current state.
+///
+/// The key in fixed word 1 must select an existing NDP row. The caller supplies
+/// that provider's current type-1 value and the object's current multiplier;
+/// this function does not invent their runtime state. Fixed word 11 is the
+/// coefficient loaded by selector `0x300`. Native code multiplies in the order
+/// `provider_now * coefficient * multiplier_now`.
+///
+/// The source x87 code returns a value with an extended exponent range even
+/// though it uses 24-bit precision. This f32 helper therefore accepts only
+/// finite normal-or-zero operands and normal-or-exact-zero intermediate and
+/// final products. It rejects subnormal or out-of-f32-range products rather
+/// than silently underflowing or overflowing differently from x87.
+pub fn sample_control_resource_device_selector_300(
+ record: &PreparedControlTypedRecord,
+ node_data: Option<&PreparedControlNodeDataTable>,
+ provider_now: Option<f32>,
+ multiplier_now: f32,
+) -> Result<f32, ControlSamplingError> {
+ if record.tag != 26 {
+ return Err(ControlSamplingError::UnsupportedResourceDeviceTag { actual: record.tag });
+ }
+
+ let key = record.fixed_words[1];
+ let Some(node_data) = node_data else {
+ return Err(ControlSamplingError::MissingResourceDeviceNodeData { key });
+ };
+ if !usize::try_from(key).is_ok_and(|key| key < node_data.rows.len()) {
+ return Err(ControlSamplingError::ResourceDeviceNodeDataKeyOutOfRange {
+ key,
+ row_count: node_data.rows.len(),
+ });
+ }
+ let provider_now =
+ provider_now.ok_or(ControlSamplingError::MissingResourceDeviceProviderValue { key })?;
+ let base = f32::from_bits(record.fixed_words[11]);
+
+ for (operand, value) in [
+ ("provider", provider_now),
+ ("base coefficient", base),
+ ("multiplier", multiplier_now),
+ ] {
+ if !value.is_finite() || (value != 0.0 && !value.is_normal()) {
+ return Err(ControlSamplingError::InvalidResourceDeviceOperand { operand });
+ }
+ }
+
+ let provider_times_base =
+ native_range_compatible_product(provider_now, base, "provider times base")?;
+ native_range_compatible_product(provider_times_base, multiplier_now, "then multiplier")
+}
+
+fn native_range_compatible_product(
+ lhs: f32,
+ rhs: f32,
+ stage: &'static str,
+) -> Result<f32, ControlSamplingError> {
+ let exact = f64::from(lhs) * f64::from(rhs);
+ let absolute = exact.abs();
+ let minimum_normal = f64::from(f32::MIN_POSITIVE);
+ let maximum = f64::from(f32::MAX);
+ if exact != 0.0 && (absolute < minimum_normal || absolute > maximum) {
+ return Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange { stage });
+ }
+
+ Ok(lhs * rhs)
+}
+
/// Samples per-node animation times using the decoded Control timeline.
///
/// `native_clock_ms` is the absolute u32 millisecond clock passed to native
@@ -4141,6 +4367,297 @@ mod tests {
// Preserve the authored CTLD fixture bits; this is not the `f32` value of tau.
const AUTHORED_TAG21_RAW_PARAMETER_3: f32 = f32::from_bits(0x40C8_F5C3);
+ fn behavior_var_row(name: &[u8], type_index: u32, first: u32, second: u32) -> Vec<u8> {
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&4_u32.to_le_bytes());
+ bytes.extend_from_slice(&type_index.to_le_bytes());
+ bytes.extend_from_slice(&0_u32.to_le_bytes());
+ bytes.extend_from_slice(&(name.len() as u32).to_le_bytes());
+ bytes.extend_from_slice(name);
+ bytes.extend_from_slice(&0_u32.to_le_bytes()); // Native transfer rows use an empty type-name.
+ bytes.extend_from_slice(&first.to_le_bytes());
+ bytes.extend_from_slice(&second.to_le_bytes());
+ bytes.extend_from_slice(&0_u32.to_le_bytes());
+ bytes.extend_from_slice(&0_u32.to_le_bytes());
+ bytes
+ }
+
+ fn behavior_var(rows: &[Vec<u8>]) -> Vec<u8> {
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&(rows.len() as u32).to_le_bytes());
+ for row in rows {
+ bytes.extend_from_slice(row);
+ }
+ bytes
+ }
+
+ #[test]
+ fn behavior_profile_decoder_uses_first_exact_rows_second_blob_and_float_kind() {
+ let profile = behavior_var(&[
+ behavior_var_row(b"transfer_ore_off_board", 3, 0x1111_1111, 0x2222_2222),
+ behavior_var_row(
+ b"Transfer_Ore_OffBoard\0legacy-suffix",
+ 3,
+ 0x3333_3333,
+ 0x3f80_0000,
+ ),
+ behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0x4444_4444, 0x4000_0000),
+ behavior_var_row(b"Transfer_Power_Out", 3, 0x5555_5555, 0x4120_0000),
+ ]);
+
+ assert_eq!(
+ decode_behavior_profile_rates(&profile),
+ Ok(BehaviorProfileRates {
+ transfer_ore_off_board_bits: 0x3f80_0000,
+ transfer_power_out_bits: 0x4120_0000,
+ })
+ );
+ }
+
+ #[test]
+ fn behavior_profile_decoder_rejects_every_truncation_and_trailing_bytes() {
+ let valid = behavior_var(&[
+ behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0, 0x3f80_0000),
+ behavior_var_row(b"Transfer_Power_Out", 3, 0, 0x4120_0000),
+ ]);
+ for end in 0..valid.len() {
+ assert!(
+ decode_behavior_profile_rates(&valid[..end]).is_err(),
+ "prefix ending at byte {end} was accepted"
+ );
+ }
+
+ let mut with_trailing = valid.clone();
+ with_trailing.push(0);
+ assert!(matches!(
+ decode_behavior_profile_rates(&with_trailing),
+ Err(BehaviorProfileVarError::Decode(
+ fparkan_binary::DecodeError::TrailingBytes { .. }
+ ))
+ ));
+ }
+
+ #[test]
+ fn behavior_profile_decoder_requires_both_registered_float_fields() {
+ let profile = behavior_var(&[behavior_var_row(b"Transfer_Power_Out", 3, 0, 0x4120_0000)]);
+ assert_eq!(
+ decode_behavior_profile_rates(&profile),
+ Err(BehaviorProfileVarError::MissingField {
+ name: "Transfer_Ore_OffBoard"
+ })
+ );
+
+ let profile = behavior_var(&[
+ behavior_var_row(b"Transfer_Ore_OffBoard", 3, 0, 0x3f80_0000),
+ behavior_var_row(b"Transfer_Power_Out", 1, 0, 0x4120_0000),
+ ]);
+ assert!(matches!(
+ decode_behavior_profile_rates(&profile),
+ Err(BehaviorProfileVarError::InvalidFloatField {
+ name: "Transfer_Power_Out",
+ size: 4,
+ type_index: 1,
+ })
+ ));
+ }
+
+ fn resource_device_record(tag: u32, key: u32, base_bits: u32) -> PreparedControlTypedRecord {
+ let mut fixed_words = [0; 44];
+ fixed_words[1] = key;
+ fixed_words[11] = base_bits;
+ PreparedControlTypedRecord {
+ tag,
+ fixed_words,
+ values: Vec::new(),
+ string_bytes: Vec::new(),
+ gate_provider: PreparedControlGateProvider::NotApplicable,
+ }
+ }
+
+ fn resource_device_node_data_table(row_count: usize) -> PreparedControlNodeDataTable {
+ PreparedControlNodeDataTable {
+ source: ResourceKey {
+ archive: parse_path("parts.rlb").expect("NDP archive"),
+ name: resource_name(b"resource-device.ndp"),
+ type_id: None,
+ },
+ rows: vec![
+ PreparedControlNodeDataRow {
+ raw_flags: 0,
+ max_volume: 0.0,
+ mobility_per_volume: 0.0,
+ effect: PreparedControlResourcePair {
+ archive: [0; 32],
+ name: [0; 32],
+ },
+ };
+ row_count
+ ],
+ }
+ }
+
+ #[test]
+ fn tag26_selector_300_multiplies_provider_then_fixed_base_then_multiplier() {
+ let record = resource_device_record(26, 1, 0x3D25_AE4A);
+ let node_data = resource_device_node_data_table(2);
+ let provider_now = f32::from_bits(0x3FE6_982F);
+ let multiplier_now = f32::from_bits(0x4071_5539);
+
+ let sampled = sample_control_resource_device_selector_300(
+ &record,
+ Some(&node_data),
+ Some(provider_now),
+ multiplier_now,
+ )
+ .expect("valid tag-26 resource-device sample");
+
+ assert_eq!(sampled.to_bits(), 0x3E8C_B02E);
+ assert_ne!(
+ sampled.to_bits(),
+ ((provider_now * multiplier_now) * f32::from_bits(record.fixed_words[11])).to_bits(),
+ "the original selector multiplies the provider by the record base first"
+ );
+ }
+
+ #[test]
+ fn tag26_selector_300_requires_its_ndp_row_and_explicit_provider() {
+ let record = resource_device_record(26, 1, 2.0_f32.to_bits());
+ let one_row = resource_device_node_data_table(1);
+ assert_eq!(
+ sample_control_resource_device_selector_300(&record, Some(&one_row), Some(3.0), 4.0,),
+ Err(ControlSamplingError::ResourceDeviceNodeDataKeyOutOfRange {
+ key: 1,
+ row_count: 1,
+ })
+ );
+
+ let two_rows = resource_device_node_data_table(2);
+ assert_eq!(
+ sample_control_resource_device_selector_300(&record, Some(&two_rows), None, 4.0),
+ Err(ControlSamplingError::MissingResourceDeviceProviderValue { key: 1 })
+ );
+ assert_eq!(
+ sample_control_resource_device_selector_300(&record, None, Some(3.0), 4.0),
+ Err(ControlSamplingError::MissingResourceDeviceNodeData { key: 1 })
+ );
+ }
+
+ #[test]
+ fn tag26_selector_300_rejects_other_tags_and_extended_x87_ranges() {
+ let node_data = resource_device_node_data_table(1);
+ let tag21 = resource_device_record(21, 0, 1.0_f32.to_bits());
+ assert_eq!(
+ sample_control_resource_device_selector_300(&tag21, Some(&node_data), Some(1.0), 1.0),
+ Err(ControlSamplingError::UnsupportedResourceDeviceTag { actual: 21 })
+ );
+
+ let tag26 = resource_device_record(26, 0, f32::MIN_POSITIVE.to_bits());
+ assert_eq!(
+ sample_control_resource_device_selector_300(
+ &tag26,
+ Some(&node_data),
+ Some(f32::MIN_POSITIVE),
+ 1.0,
+ ),
+ Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange {
+ stage: "provider times base",
+ })
+ );
+ }
+
+ #[test]
+ fn tag26_selector_300_preserves_negative_and_signed_zero_values() {
+ let node_data = resource_device_node_data_table(1);
+ let record = resource_device_record(26, 0, 0.5_f32.to_bits());
+ let negative =
+ sample_control_resource_device_selector_300(&record, Some(&node_data), Some(-2.0), 4.0)
+ .expect("negative factors are not clamped");
+ assert_eq!(negative, -4.0);
+
+ let negative_zero = sample_control_resource_device_selector_300(
+ &resource_device_record(26, 0, 2.0_f32.to_bits()),
+ Some(&node_data),
+ Some(-0.0),
+ 3.0,
+ )
+ .expect("signed zero is an exact native input");
+ assert_eq!(negative_zero.to_bits(), (-0.0_f32).to_bits());
+
+ let positive_zero = sample_control_resource_device_selector_300(
+ &resource_device_record(26, 0, 2.0_f32.to_bits()),
+ Some(&node_data),
+ Some(0.0),
+ 3.0,
+ )
+ .expect("positive zero is an exact native input");
+ assert_eq!(positive_zero.to_bits(), 0.0_f32.to_bits());
+ }
+
+ #[test]
+ fn tag26_selector_300_rejects_nonfinite_and_subnormal_operands() {
+ let node_data = resource_device_node_data_table(1);
+ for (operand, provider_now, multiplier_now, base_bits) in [
+ ("provider", f32::NAN, 1.0, 1.0_f32.to_bits()),
+ ("multiplier", 1.0, f32::INFINITY, 1.0_f32.to_bits()),
+ ("base coefficient", 1.0, 1.0, 1),
+ ] {
+ let record = resource_device_record(26, 0, base_bits);
+ assert_eq!(
+ sample_control_resource_device_selector_300(
+ &record,
+ Some(&node_data),
+ Some(provider_now),
+ multiplier_now,
+ ),
+ Err(ControlSamplingError::InvalidResourceDeviceOperand { operand })
+ );
+ }
+ }
+
+ #[test]
+ fn tag26_selector_300_rejects_extended_intermediate_and_final_ranges() {
+ let node_data = resource_device_node_data_table(1);
+ let provider = f32::MAX;
+ let overflowing_intermediate = resource_device_record(26, 0, 2.0_f32.to_bits());
+ assert_eq!(
+ sample_control_resource_device_selector_300(
+ &overflowing_intermediate,
+ Some(&node_data),
+ Some(provider),
+ 0.25,
+ ),
+ Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange {
+ stage: "provider times base",
+ })
+ );
+
+ let underflowing_intermediate = resource_device_record(26, 0, f32::MIN_POSITIVE.to_bits());
+ assert_eq!(
+ sample_control_resource_device_selector_300(
+ &underflowing_intermediate,
+ Some(&node_data),
+ Some(f32::MIN_POSITIVE),
+ f32::MAX,
+ ),
+ Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange {
+ stage: "provider times base",
+ })
+ );
+
+ let subnormal_final = resource_device_record(26, 0, 1.0_f32.to_bits());
+ assert_eq!(
+ sample_control_resource_device_selector_300(
+ &subnormal_final,
+ Some(&node_data),
+ Some(f32::MIN_POSITIVE),
+ 0.5,
+ ),
+ Err(ControlSamplingError::UnsupportedResourceDeviceFloatRange {
+ stage: "then multiplier",
+ })
+ );
+ }
+
fn tag21_fullcycle_control() -> PreparedComponentControl {
let source = ResourceKey {
archive: parse_path("parts.rlb").expect("archive"),
@@ -6166,6 +6683,60 @@ mod tests {
#[test]
#[ignore = "requires licensed corpus"]
+ fn tag26_selector_300_uses_a_real_control_record_and_ndp_key() {
+ let root = fixture_root("IS");
+ let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
+ let repository = CachedResourceRepository::new(Arc::clone(&vfs));
+ let roots = [resource_name(b"UNITS/BUILDS/MINE/min_l_n1.dat")];
+ let (_graph, prototypes) =
+ build_prototype_graph(&repository, vfs.as_ref(), &roots).expect("prototype graph");
+ let assets = prepare_mission_assets_with_repository(
+ &repository,
+ &[0..prototypes.len()],
+ &prototypes,
+ )
+ .expect("prepared licensed unit controls");
+
+ let mut sampled_records = 0;
+ for control in assets.object_component_controls.iter().flatten().flatten() {
+ for record in control
+ .typed_records
+ .iter()
+ .filter(|record| record.tag == 26)
+ {
+ let node_data = control
+ .node_data
+ .as_ref()
+ .expect("tag-26 control has its explicit NDP table");
+ assert_eq!(control.source.name.0, b"fr_l_mine.ctl");
+ assert_eq!(node_data.source.name.0, b"fr_l_mine.ndp");
+ assert_eq!(node_data.rows.len(), 22);
+ assert_eq!(record.fixed_words[1], 2);
+ assert_eq!(record.fixed_words[11], 0x3F80_0000);
+ let sample = sample_control_resource_device_selector_300(
+ record,
+ Some(node_data),
+ Some(1.0),
+ 1.0,
+ )
+ .expect("real tag-26 key selects an NDP row");
+ assert_eq!(sample.to_bits(), record.fixed_words[11]);
+ println!(
+ "control={}: ndp={}: key={} rows={} coefficient=0x{:08x}",
+ String::from_utf8_lossy(&control.source.name.0),
+ String::from_utf8_lossy(&node_data.source.name.0),
+ record.fixed_words[1],
+ node_data.rows.len(),
+ record.fixed_words[11],
+ );
+ sampled_records += 1;
+ }
+ }
+ assert_eq!(sampled_records, 1);
+ }
+
+ #[test]
+ #[ignore = "requires licensed corpus"]
fn repository_plan_deduplicates_duplicate_visuals_but_graph_preserves_requests() {
let root = fixture_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));