This commit is contained in:
@@ -905,6 +905,34 @@ pub struct PreparedComponentControl {
|
|||||||
pub bindings: Vec<PreparedControlNodeBinding>,
|
pub bindings: Vec<PreparedControlNodeBinding>,
|
||||||
/// Parsed native effect rows in this component's control resource.
|
/// Parsed native effect rows in this component's control resource.
|
||||||
pub effect_section: PreparedControlEffectSection,
|
pub effect_section: PreparedControlEffectSection,
|
||||||
|
/// Component-local points selected for parsed effect rows, when present.
|
||||||
|
pub point_list: Option<PreparedControlPointList>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decoded point records selected by a component's Control configuration.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PreparedControlPointList {
|
||||||
|
/// CPT resource paired with the component control resource.
|
||||||
|
pub source: ResourceKey,
|
||||||
|
/// Point records in their original component-local order.
|
||||||
|
pub points: Vec<PreparedControlPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One 36-byte native CPT point record and its parallel fixed-width name.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PreparedControlPoint {
|
||||||
|
/// Raw native flag bits.
|
||||||
|
pub raw_flags: u32,
|
||||||
|
/// Raw signed 32-bit word at byte offset +4 in the native record.
|
||||||
|
pub raw_signed_field_04: i32,
|
||||||
|
/// Raw signed 32-bit word at byte offset +8 in the native record.
|
||||||
|
pub raw_signed_field_08: i32,
|
||||||
|
/// Three stored coordinate values at byte offset +0x0C.
|
||||||
|
pub position: [f32; 3],
|
||||||
|
/// Three stored direction values at byte offset +0x18.
|
||||||
|
pub direction: [f32; 3],
|
||||||
|
/// Parallel raw 32-byte name entry, including NUL padding.
|
||||||
|
pub name: [u8; 32],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Status of the trailing typed/effect data in a CTLD resource.
|
/// Status of the trailing typed/effect data in a CTLD resource.
|
||||||
@@ -1534,6 +1562,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
let control = prepare_component_control_from_resource(
|
let control = prepare_component_control_from_resource(
|
||||||
repository,
|
repository,
|
||||||
source,
|
source,
|
||||||
|
prototype.point_resource.clone(),
|
||||||
model.validated.node_count,
|
model.validated.node_count,
|
||||||
prototype_index,
|
prototype_index,
|
||||||
)?;
|
)?;
|
||||||
@@ -2831,13 +2860,125 @@ fn read_optional_key<R: ResourceRepository>(
|
|||||||
fn prepare_component_control_from_resource<R: ResourceRepository>(
|
fn prepare_component_control_from_resource<R: ResourceRepository>(
|
||||||
repository: &R,
|
repository: &R,
|
||||||
source: ResourceKey,
|
source: ResourceKey,
|
||||||
|
point_source: Option<ResourceKey>,
|
||||||
model_node_count: usize,
|
model_node_count: usize,
|
||||||
prototype_index: usize,
|
prototype_index: usize,
|
||||||
) -> Result<Option<PreparedComponentControl>, AssetError> {
|
) -> Result<Option<PreparedComponentControl>, AssetError> {
|
||||||
let Some(payload) = read_optional_key(repository, &source, Some("component control"))? else {
|
let Some(payload) = read_optional_key(repository, &source, Some("component control"))? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
decode_component_control_payload(&payload, source, model_node_count, prototype_index).map(Some)
|
let mut control =
|
||||||
|
decode_component_control_payload(&payload, source, model_node_count, prototype_index)?;
|
||||||
|
let has_effect_rows = matches!(
|
||||||
|
&control.effect_section,
|
||||||
|
PreparedControlEffectSection::Parsed(rows) if !rows.is_empty()
|
||||||
|
);
|
||||||
|
let Some(point_source) = point_source.filter(|_| has_effect_rows) else {
|
||||||
|
return Ok(Some(control));
|
||||||
|
};
|
||||||
|
let Some(point_payload) =
|
||||||
|
read_optional_key(repository, &point_source, Some("component point list"))?
|
||||||
|
else {
|
||||||
|
return Ok(Some(control));
|
||||||
|
};
|
||||||
|
control.point_list = Some(decode_component_point_list(
|
||||||
|
&point_payload,
|
||||||
|
point_source,
|
||||||
|
prototype_index,
|
||||||
|
)?);
|
||||||
|
Ok(Some(control))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_component_point_list(
|
||||||
|
payload: &[u8],
|
||||||
|
source: ResourceKey,
|
||||||
|
prototype_index: usize,
|
||||||
|
) -> Result<PreparedControlPointList, AssetError> {
|
||||||
|
const HEADER_BYTES: usize = 4;
|
||||||
|
const POINT_BYTES: usize = 36;
|
||||||
|
const NAME_BYTES: usize = 32;
|
||||||
|
const POINT_LIMIT: usize = 65_536;
|
||||||
|
|
||||||
|
let invalid = |message: &str| {
|
||||||
|
AssetError::InvalidPrototype(format!(
|
||||||
|
"prototype {prototype_index} point list {}:{}: {message}",
|
||||||
|
source.archive.as_str(),
|
||||||
|
String::from_utf8_lossy(&source.name.0),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let point_count =
|
||||||
|
usize::try_from(control_u32_at(payload, 0).ok_or_else(|| invalid("missing point count"))?)
|
||||||
|
.map_err(|_| invalid("point count overflows usize"))?;
|
||||||
|
if point_count > POINT_LIMIT {
|
||||||
|
return Err(invalid("point count exceeds the supported limit"));
|
||||||
|
}
|
||||||
|
let point_bytes = point_count
|
||||||
|
.checked_mul(POINT_BYTES)
|
||||||
|
.ok_or_else(|| invalid("point table byte length overflows"))?;
|
||||||
|
let names_start = HEADER_BYTES
|
||||||
|
.checked_add(point_bytes)
|
||||||
|
.ok_or_else(|| invalid("point name table offset overflows"))?;
|
||||||
|
let names_bytes = point_count
|
||||||
|
.checked_mul(NAME_BYTES)
|
||||||
|
.ok_or_else(|| invalid("point name table byte length overflows"))?;
|
||||||
|
let expected_len = names_start
|
||||||
|
.checked_add(names_bytes)
|
||||||
|
.ok_or_else(|| invalid("point list end offset overflows"))?;
|
||||||
|
if payload.len() != expected_len {
|
||||||
|
return Err(invalid(
|
||||||
|
"payload length does not match its point and name counts",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut points = Vec::with_capacity(point_count);
|
||||||
|
for index in 0..point_count {
|
||||||
|
let point_start = HEADER_BYTES + index * POINT_BYTES;
|
||||||
|
let position = control_vec3_at(payload, point_start + 12)
|
||||||
|
.expect("complete CPT point record was checked");
|
||||||
|
let direction = control_vec3_at(payload, point_start + 24)
|
||||||
|
.expect("complete CPT point record was checked");
|
||||||
|
if position
|
||||||
|
.into_iter()
|
||||||
|
.chain(direction)
|
||||||
|
.any(|value| !value.is_finite())
|
||||||
|
{
|
||||||
|
return Err(invalid(
|
||||||
|
"point position or direction contains a non-finite value",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let name_start = names_start + index * NAME_BYTES;
|
||||||
|
points.push(PreparedControlPoint {
|
||||||
|
raw_flags: control_u32_at(payload, point_start).expect("checked point flags"),
|
||||||
|
raw_signed_field_04: control_i32_at(payload, point_start + 4)
|
||||||
|
.expect("checked signed point word at +4"),
|
||||||
|
raw_signed_field_08: control_i32_at(payload, point_start + 8)
|
||||||
|
.expect("checked signed point word at +8"),
|
||||||
|
position,
|
||||||
|
direction,
|
||||||
|
name: payload[name_start..name_start + NAME_BYTES]
|
||||||
|
.try_into()
|
||||||
|
.expect("fixed point name is 32 bytes"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(PreparedControlPointList { source, points })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_vec3_at(payload: &[u8], offset: usize) -> Option<[f32; 3]> {
|
||||||
|
Some([
|
||||||
|
control_f32_at(payload, offset)?,
|
||||||
|
control_f32_at(payload, offset.checked_add(4)?)?,
|
||||||
|
control_f32_at(payload, offset.checked_add(8)?)?,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_f32_at(payload: &[u8], offset: usize) -> Option<f32> {
|
||||||
|
Some(f32::from_bits(control_u32_at(payload, offset)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_i32_at(payload: &[u8], offset: usize) -> Option<i32> {
|
||||||
|
Some(i32::from_le_bytes(
|
||||||
|
control_u32_at(payload, offset)?.to_le_bytes(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] = &[1, 8, 9, 12, 13, 15, 19, 21, 25, 26];
|
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] = &[1, 8, 9, 12, 13, 15, 19, 21, 25, 26];
|
||||||
@@ -3156,6 +3297,7 @@ fn decode_component_control_payload(
|
|||||||
source,
|
source,
|
||||||
bindings,
|
bindings,
|
||||||
effect_section,
|
effect_section,
|
||||||
|
point_list: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3455,6 +3597,206 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_loads_explicit_cpt_points_for_effect_offsets() {
|
||||||
|
let mut control = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
||||||
|
control[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
control.resize(control.len() + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(
|
||||||
|
&mut control,
|
||||||
|
&[
|
||||||
|
effect_row(
|
||||||
|
4,
|
||||||
|
[0; 3],
|
||||||
|
[104, 105, 106],
|
||||||
|
17,
|
||||||
|
"effects.rlb",
|
||||||
|
"f_smalllight_y",
|
||||||
|
),
|
||||||
|
effect_row(
|
||||||
|
4,
|
||||||
|
[0; 3],
|
||||||
|
[119, 120, 121],
|
||||||
|
3300,
|
||||||
|
"effects.rlb",
|
||||||
|
"smoke_fr_02",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let mut cpt = cpt_payload(122);
|
||||||
|
write_cpt_point(
|
||||||
|
&mut cpt,
|
||||||
|
104,
|
||||||
|
0,
|
||||||
|
23,
|
||||||
|
23,
|
||||||
|
[-12.964_036, 12.991_658, -21.119_995],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
"Sign_Recharge1",
|
||||||
|
);
|
||||||
|
write_cpt_point(
|
||||||
|
&mut cpt,
|
||||||
|
119,
|
||||||
|
0x8000_0000,
|
||||||
|
24,
|
||||||
|
24,
|
||||||
|
[0.0, 0.166_994_02, 32.7812],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
"Smoke_w",
|
||||||
|
);
|
||||||
|
let repository = repository_with_archives(&[
|
||||||
|
("controls.rlb", &[(b"parts.ctl", &control)]),
|
||||||
|
("points.rlb", &[(b"attachments.cpt", &cpt)]),
|
||||||
|
]);
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("control archive"),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let point_source = ResourceKey {
|
||||||
|
archive: parse_path("points.rlb").expect("point archive"),
|
||||||
|
name: resource_name(b"attachments.cpt"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let control =
|
||||||
|
prepare_component_control_from_resource(&repository, source, Some(point_source), 2, 0)
|
||||||
|
.expect("loaded CTL and its explicit CPT")
|
||||||
|
.expect("CTL entry");
|
||||||
|
let point_list = control.point_list.as_ref().expect("point list is attached");
|
||||||
|
|
||||||
|
assert_eq!(point_list.source.archive.as_str(), "points.rlb");
|
||||||
|
assert_eq!(point_list.source.name, resource_name(b"attachments.cpt"));
|
||||||
|
assert_eq!(point_list.points.len(), 122);
|
||||||
|
let PreparedControlEffectSection::Parsed(rows) = &control.effect_section else {
|
||||||
|
panic!("effect rows parsed");
|
||||||
|
};
|
||||||
|
assert_eq!(rows[0].point_offsets, [104, 105, 106]);
|
||||||
|
assert_eq!(rows[1].point_offsets, [119, 120, 121]);
|
||||||
|
assert_eq!(point_list.points[104].raw_signed_field_04, 23);
|
||||||
|
assert_eq!(point_list.points[104].raw_signed_field_08, 23);
|
||||||
|
assert_eq!(
|
||||||
|
point_list.points[104].position,
|
||||||
|
[-12.964_036, 12.991_658, -21.119_995]
|
||||||
|
);
|
||||||
|
assert_eq!(point_list.points[104].name, fixed_name("Sign_Recharge1"));
|
||||||
|
assert_eq!(point_list.points[119].raw_flags, 0x8000_0000);
|
||||||
|
assert_eq!(point_list.points[119].name, fixed_name("Smoke_w"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_point_list_rejects_bad_bounds_and_non_finite_points() {
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"parts.cpt"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let valid = cpt_payload(1);
|
||||||
|
assert!(decode_component_point_list(&valid[..valid.len() - 1], source.clone(), 0).is_err());
|
||||||
|
let mut trailing = valid.clone();
|
||||||
|
trailing.push(0);
|
||||||
|
assert!(decode_component_point_list(&trailing, source.clone(), 0).is_err());
|
||||||
|
|
||||||
|
let mut non_finite = valid;
|
||||||
|
non_finite[16..20].copy_from_slice(&f32::INFINITY.to_bits().to_le_bytes());
|
||||||
|
assert!(decode_component_point_list(&non_finite, source, 0).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_preserves_unresolved_point_offset_sentinels() {
|
||||||
|
let mut control = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
||||||
|
control[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
control.resize(control.len() + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(
|
||||||
|
&mut control,
|
||||||
|
&[effect_row(
|
||||||
|
4,
|
||||||
|
[0; 3],
|
||||||
|
[u32::MAX; 3],
|
||||||
|
17,
|
||||||
|
"effects.rlb",
|
||||||
|
"f_smalllight_y",
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
let cpt = cpt_payload(1);
|
||||||
|
let repository = repository_with_archives(&[
|
||||||
|
("controls.rlb", &[(b"parts.ctl", &control)]),
|
||||||
|
("points.rlb", &[(b"attachments.cpt", &cpt)]),
|
||||||
|
]);
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("control archive"),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let point_source = ResourceKey {
|
||||||
|
archive: parse_path("points.rlb").expect("point archive"),
|
||||||
|
name: resource_name(b"attachments.cpt"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let control =
|
||||||
|
prepare_component_control_from_resource(&repository, source, Some(point_source), 2, 0)
|
||||||
|
.expect("point-offset sentinel is retained without guessed bounds")
|
||||||
|
.expect("CTL entry");
|
||||||
|
let PreparedControlEffectSection::Parsed(rows) = control.effect_section else {
|
||||||
|
panic!("effect rows parsed");
|
||||||
|
};
|
||||||
|
assert_eq!(rows[0].point_offsets, [u32::MAX; 3]);
|
||||||
|
assert_eq!(
|
||||||
|
control
|
||||||
|
.point_list
|
||||||
|
.expect("explicit CPT loaded")
|
||||||
|
.points
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_keeps_missing_explicit_cpt_unavailable() {
|
||||||
|
let mut control = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
||||||
|
control[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
control.resize(control.len() + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(
|
||||||
|
&mut control,
|
||||||
|
&[effect_row(
|
||||||
|
4,
|
||||||
|
[0; 3],
|
||||||
|
[0; 3],
|
||||||
|
17,
|
||||||
|
"effects.rlb",
|
||||||
|
"f_smalllight_y",
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
let repository = repository_with_archives(&[("controls.rlb", &[(b"parts.ctl", &control)])]);
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("control archive"),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let missing_point_source = ResourceKey {
|
||||||
|
archive: parse_path("points.rlb").expect("point archive"),
|
||||||
|
name: resource_name(b"attachments.cpt"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let control = prepare_component_control_from_resource(
|
||||||
|
&repository,
|
||||||
|
source,
|
||||||
|
Some(missing_point_source),
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.expect("an absent optional CPT leaves rows inspectable")
|
||||||
|
.expect("CTL entry");
|
||||||
|
|
||||||
|
assert_eq!(control.point_list, None);
|
||||||
|
assert!(matches!(
|
||||||
|
control.effect_section,
|
||||||
|
PreparedControlEffectSection::Parsed(rows) if rows.len() == 1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_control_max_length_uses_wrapped_empty_tail() {
|
fn component_control_max_length_uses_wrapped_empty_tail() {
|
||||||
let source = ResourceKey {
|
let source = ResourceKey {
|
||||||
@@ -3548,6 +3890,43 @@ mod tests {
|
|||||||
target[..value.len()].copy_from_slice(value.as_bytes());
|
target[..value.len()].copy_from_slice(value.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cpt_payload(point_count: usize) -> Vec<u8> {
|
||||||
|
let mut payload = vec![0; 4 + point_count * 36 + point_count * 32];
|
||||||
|
payload[..4].copy_from_slice(
|
||||||
|
&u32::try_from(point_count)
|
||||||
|
.expect("point count")
|
||||||
|
.to_le_bytes(),
|
||||||
|
);
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_cpt_point(
|
||||||
|
payload: &mut [u8],
|
||||||
|
index: usize,
|
||||||
|
raw_flags: u32,
|
||||||
|
signed_field_04: i32,
|
||||||
|
signed_field_08: i32,
|
||||||
|
position: [f32; 3],
|
||||||
|
direction: [f32; 3],
|
||||||
|
name: &str,
|
||||||
|
) {
|
||||||
|
let count = control_u32_at(payload, 0).expect("point count") as usize;
|
||||||
|
assert!(index < count);
|
||||||
|
let point_start = 4 + index * 36;
|
||||||
|
payload[point_start..point_start + 4].copy_from_slice(&raw_flags.to_le_bytes());
|
||||||
|
payload[point_start + 4..point_start + 8].copy_from_slice(&signed_field_04.to_le_bytes());
|
||||||
|
payload[point_start + 8..point_start + 12].copy_from_slice(&signed_field_08.to_le_bytes());
|
||||||
|
for (offset, value) in position.into_iter().chain(direction).enumerate() {
|
||||||
|
let start = point_start + 12 + offset * 4;
|
||||||
|
payload[start..start + 4].copy_from_slice(&value.to_bits().to_le_bytes());
|
||||||
|
}
|
||||||
|
let names_start = 4 + count * 36;
|
||||||
|
write_fixed_name(
|
||||||
|
&mut payload[names_start + index * 32..names_start + (index + 1) * 32],
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn page_uv_transform_matches_normalized_native_atlas_record() {
|
fn page_uv_transform_matches_normalized_native_atlas_record() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3664,6 +4043,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
let second = EffectivePrototype {
|
let second = EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"mesh")),
|
||||||
@@ -3677,6 +4057,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_ne!(stable_visual_id(&first), stable_visual_id(&second));
|
assert_ne!(stable_visual_id(&first), stable_visual_id(&second));
|
||||||
@@ -3741,6 +4122,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: vec![mesh_key.clone()],
|
dependencies: vec![mesh_key.clone()],
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -3856,6 +4238,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
graph
|
graph
|
||||||
@@ -4009,6 +4392,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -4053,8 +4437,32 @@ mod tests {
|
|||||||
let mat0 = mat0_with_texture(b"TEX_A");
|
let mat0 = mat0_with_texture(b"TEX_A");
|
||||||
let texm = texm_payload();
|
let texm = texm_payload();
|
||||||
let lightmap_texm = texm_payload();
|
let lightmap_texm = texm_payload();
|
||||||
let first_control = component_control_payload(&[(0, 1.25, 2.5, 0.25, 0)]);
|
let mut first_control = component_control_payload(&[(0, 1.25, 2.5, 0.25, 0)]);
|
||||||
|
first_control[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
first_control.resize(first_control.len() + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(
|
||||||
|
&mut first_control,
|
||||||
|
&[effect_row(
|
||||||
|
4,
|
||||||
|
[0; 3],
|
||||||
|
[0; 3],
|
||||||
|
17,
|
||||||
|
"effects.rlb",
|
||||||
|
"f_smalllight_y",
|
||||||
|
)],
|
||||||
|
);
|
||||||
let second_control = component_control_payload(&[(0, 2.5, 3.75, 0.75, 0)]);
|
let second_control = component_control_payload(&[(0, 2.5, 3.75, 0.75, 0)]);
|
||||||
|
let mut points = cpt_payload(1);
|
||||||
|
write_cpt_point(
|
||||||
|
&mut points,
|
||||||
|
0,
|
||||||
|
0x8000_0000,
|
||||||
|
-23,
|
||||||
|
-24,
|
||||||
|
[2.0, 3.0, 4.0],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
"LightAnchor",
|
||||||
|
);
|
||||||
let repo = repository_with_archives_meta(&[
|
let repo = repository_with_archives_meta(&[
|
||||||
(
|
(
|
||||||
"static.rlb",
|
"static.rlb",
|
||||||
@@ -4117,6 +4525,15 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"points.rlb",
|
||||||
|
&[TestNresEntry {
|
||||||
|
name: b"attachments.cpt",
|
||||||
|
payload: &points,
|
||||||
|
type_id: 0,
|
||||||
|
attr2: 0,
|
||||||
|
}],
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
let prototypes = [
|
let prototypes = [
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
@@ -4130,6 +4547,11 @@ mod tests {
|
|||||||
name: resource_name(b"first.ctl"),
|
name: resource_name(b"first.ctl"),
|
||||||
type_id: None,
|
type_id: None,
|
||||||
}),
|
}),
|
||||||
|
point_resource: Some(ResourceKey {
|
||||||
|
archive: parse_path("points.rlb").expect("point archive"),
|
||||||
|
name: resource_name(b"attachments.cpt"),
|
||||||
|
type_id: None,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-b")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-b")),
|
||||||
@@ -4142,6 +4564,7 @@ mod tests {
|
|||||||
name: resource_name(b"second.ctl"),
|
name: resource_name(b"second.ctl"),
|
||||||
type_id: None,
|
type_id: None,
|
||||||
}),
|
}),
|
||||||
|
point_resource: None,
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-c")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree-c")),
|
||||||
@@ -4154,6 +4577,7 @@ mod tests {
|
|||||||
name: resource_name(b"missing.ctl"),
|
name: resource_name(b"missing.ctl"),
|
||||||
type_id: None,
|
type_id: None,
|
||||||
}),
|
}),
|
||||||
|
point_resource: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -4184,6 +4608,14 @@ mod tests {
|
|||||||
.name,
|
.name,
|
||||||
resource_name(b"first.ctl")
|
resource_name(b"first.ctl")
|
||||||
);
|
);
|
||||||
|
let first = controls[0].as_ref().expect("first component control");
|
||||||
|
let points = first.point_list.as_ref().expect("explicit CPT was loaded");
|
||||||
|
assert_eq!(points.source.archive.as_str(), "points.rlb");
|
||||||
|
assert_eq!(points.source.name, resource_name(b"attachments.cpt"));
|
||||||
|
assert_eq!(points.points.len(), 1);
|
||||||
|
assert_eq!(points.points[0].raw_signed_field_04, -23);
|
||||||
|
assert_eq!(points.points[0].raw_signed_field_08, -24);
|
||||||
|
assert_eq!(points.points[0].name, fixed_name("LightAnchor"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
controls[1]
|
controls[1]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -4239,6 +4671,7 @@ mod tests {
|
|||||||
name: resource_name(b"marker.ctl"),
|
name: resource_name(b"marker.ctl"),
|
||||||
type_id: None,
|
type_id: None,
|
||||||
}),
|
}),
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let assets = prepare_mission_assets_with_repository(&repository, &[0..1], &[prototype])
|
let assets = prepare_mission_assets_with_repository(&repository, &[0..1], &[prototype])
|
||||||
@@ -4434,6 +4867,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: vec![mesh_key.clone()],
|
dependencies: vec![mesh_key.clone()],
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
};
|
};
|
||||||
let mut graph = prototype_graph_for_mesh(&prototype);
|
let mut graph = prototype_graph_for_mesh(&prototype);
|
||||||
let mut report = PrototypeGraphReport {
|
let mut report = PrototypeGraphReport {
|
||||||
@@ -4643,6 +5077,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
},
|
},
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: fparkan_prototype::PrototypeKey(resource_name(b"tree_b")),
|
key: fparkan_prototype::PrototypeKey(resource_name(b"tree_b")),
|
||||||
@@ -4655,6 +5090,7 @@ mod tests {
|
|||||||
source_type: None,
|
source_type: None,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource: None,
|
control_resource: None,
|
||||||
|
point_resource: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
(repo, prototypes)
|
(repo, prototypes)
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ pub struct EffectivePrototype {
|
|||||||
/// state belongs to each component instance and must not alter shared
|
/// state belongs to each component instance and must not alter shared
|
||||||
/// mesh/material cache identity.
|
/// mesh/material cache identity.
|
||||||
pub control_resource: Option<ResourceKey>,
|
pub control_resource: Option<ResourceKey>,
|
||||||
|
/// Explicit component point resource referenced by this prototype's
|
||||||
|
/// resolved `objects.rlb` registry inheritance chain, when present.
|
||||||
|
pub point_resource: Option<ResourceKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prototype resolution source.
|
/// Prototype resolution source.
|
||||||
@@ -1141,7 +1144,14 @@ fn resolve_archive_model(
|
|||||||
let Some(mesh) = find_mesh_resource(repository, archive, model_key)? else {
|
let Some(mesh) = find_mesh_resource(repository, archive, model_key)? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(Some(effective(model_key.clone(), mesh, source, None, None)))
|
Ok(Some(effective(
|
||||||
|
model_key.clone(),
|
||||||
|
mesh,
|
||||||
|
source,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_objects_registry_model(
|
fn resolve_objects_registry_model(
|
||||||
@@ -1165,6 +1175,7 @@ fn resolve_objects_registry_model(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let control_resource = resolve_explicit_control_resource(object_key, &refs)?;
|
let control_resource = resolve_explicit_control_resource(object_key, &refs)?;
|
||||||
|
let point_resource = resolve_explicit_point_resource(object_key, &refs)?;
|
||||||
|
|
||||||
let mut missing_mesh_refs = Vec::new();
|
let mut missing_mesh_refs = Vec::new();
|
||||||
for item in refs.iter().filter(|item| is_explicit_mesh_ref(item)) {
|
for item in refs.iter().filter(|item| is_explicit_mesh_ref(item)) {
|
||||||
@@ -1175,6 +1186,7 @@ fn resolve_objects_registry_model(
|
|||||||
cstr_bytes(&item.resource_raw),
|
cstr_bytes(&item.resource_raw),
|
||||||
source_type,
|
source_type,
|
||||||
control_resource.clone(),
|
control_resource.clone(),
|
||||||
|
point_resource.clone(),
|
||||||
)? {
|
)? {
|
||||||
return Ok(Some(prototype));
|
return Ok(Some(prototype));
|
||||||
}
|
}
|
||||||
@@ -1195,6 +1207,7 @@ fn resolve_objects_registry_model(
|
|||||||
source_type,
|
source_type,
|
||||||
dependencies: Vec::new(),
|
dependencies: Vec::new(),
|
||||||
control_resource,
|
control_resource,
|
||||||
|
point_resource,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,6 +1275,7 @@ fn resolve_object_ref_model(
|
|||||||
model_name: &[u8],
|
model_name: &[u8],
|
||||||
source_type: Option<u32>,
|
source_type: Option<u32>,
|
||||||
control_resource: Option<ResourceKey>,
|
control_resource: Option<ResourceKey>,
|
||||||
|
point_resource: Option<ResourceKey>,
|
||||||
) -> Result<Option<EffectivePrototype>, PrototypeError> {
|
) -> Result<Option<EffectivePrototype>, PrototypeError> {
|
||||||
let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
let archive = normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
||||||
let Some(mesh) = find_mesh_resource(repository, &archive, &ResourceName(model_name.to_vec()))?
|
let Some(mesh) = find_mesh_resource(repository, &archive, &ResourceName(model_name.to_vec()))?
|
||||||
@@ -1274,6 +1288,7 @@ fn resolve_object_ref_model(
|
|||||||
PrototypeSource::ObjectsRegistry,
|
PrototypeSource::ObjectsRegistry,
|
||||||
source_type,
|
source_type,
|
||||||
control_resource,
|
control_resource,
|
||||||
|
point_resource,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,18 +1300,37 @@ fn is_explicit_control_ref(item: &ObjectRefRecord) -> bool {
|
|||||||
has_extension_bytes(cstr_bytes(&item.resource_raw), b"ctl")
|
has_extension_bytes(cstr_bytes(&item.resource_raw), b"ctl")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_explicit_point_ref(item: &ObjectRefRecord) -> bool {
|
||||||
|
has_extension_bytes(cstr_bytes(&item.resource_raw), b"cpt")
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_explicit_control_resource(
|
fn resolve_explicit_control_resource(
|
||||||
requested: &ResourceName,
|
requested: &ResourceName,
|
||||||
refs: &[ObjectRefRecord],
|
refs: &[ObjectRefRecord],
|
||||||
|
) -> Result<Option<ResourceKey>, PrototypeError> {
|
||||||
|
resolve_explicit_resource(requested, refs, is_explicit_control_ref, "control")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_explicit_point_resource(
|
||||||
|
requested: &ResourceName,
|
||||||
|
refs: &[ObjectRefRecord],
|
||||||
|
) -> Result<Option<ResourceKey>, PrototypeError> {
|
||||||
|
resolve_explicit_resource(requested, refs, is_explicit_point_ref, "point")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_explicit_resource(
|
||||||
|
requested: &ResourceName,
|
||||||
|
refs: &[ObjectRefRecord],
|
||||||
|
is_match: fn(&ObjectRefRecord) -> bool,
|
||||||
|
label: &str,
|
||||||
) -> Result<Option<ResourceKey>, PrototypeError> {
|
) -> Result<Option<ResourceKey>, PrototypeError> {
|
||||||
let mut resolved = Vec::new();
|
let mut resolved = Vec::new();
|
||||||
for item in refs.iter().filter(|item| is_explicit_control_ref(item)) {
|
for item in refs.iter().filter(|item| is_match(item)) {
|
||||||
let archive =
|
let archive =
|
||||||
normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
normalized_path_from_name(&ResourceName(cstr_bytes(&item.archive_raw).to_vec()))?;
|
||||||
let name = resource_name(cstr_bytes(&item.resource_raw));
|
let name = resource_name(cstr_bytes(&item.resource_raw));
|
||||||
// CTLD is an optional pose sidecar. Keep the exact registry reference
|
// Keep exact registry references; asset preparation decides how to
|
||||||
// here, then let asset preparation treat a missing archive/entry as an
|
// handle an absent optional sidecar and validates present payloads.
|
||||||
// absent override while still rejecting a present malformed payload.
|
|
||||||
let key = ResourceKey {
|
let key = ResourceKey {
|
||||||
archive,
|
archive,
|
||||||
name,
|
name,
|
||||||
@@ -1313,7 +1347,7 @@ fn resolve_explicit_control_resource(
|
|||||||
0 => Ok(None),
|
0 => Ok(None),
|
||||||
1 => Ok(resolved.pop()),
|
1 => Ok(resolved.pop()),
|
||||||
_ => Err(PrototypeError::Resource(ResourceError::Format(format!(
|
_ => Err(PrototypeError::Resource(ResourceError::Format(format!(
|
||||||
"prototype {} has ambiguous explicit control resources: {}",
|
"prototype {} has ambiguous explicit {label} resources: {}",
|
||||||
String::from_utf8_lossy(&requested.0),
|
String::from_utf8_lossy(&requested.0),
|
||||||
resolved
|
resolved
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1386,6 +1420,7 @@ fn effective(
|
|||||||
source: PrototypeSource,
|
source: PrototypeSource,
|
||||||
source_type: Option<u32>,
|
source_type: Option<u32>,
|
||||||
control_resource: Option<ResourceKey>,
|
control_resource: Option<ResourceKey>,
|
||||||
|
point_resource: Option<ResourceKey>,
|
||||||
) -> EffectivePrototype {
|
) -> EffectivePrototype {
|
||||||
EffectivePrototype {
|
EffectivePrototype {
|
||||||
key: PrototypeKey(requested),
|
key: PrototypeKey(requested),
|
||||||
@@ -1394,6 +1429,7 @@ fn effective(
|
|||||||
source_type,
|
source_type,
|
||||||
dependencies: vec![mesh],
|
dependencies: vec![mesh],
|
||||||
control_resource,
|
control_resource,
|
||||||
|
point_resource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2133,6 +2169,7 @@ mod tests {
|
|||||||
b"parent_proto".as_slice(),
|
b"parent_proto".as_slice(),
|
||||||
build_object_refs(&[
|
build_object_refs(&[
|
||||||
(b"static.rlb".as_slice(), b"parent_proto.msh".as_slice()),
|
(b"static.rlb".as_slice(), b"parent_proto.msh".as_slice()),
|
||||||
|
(b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()),
|
||||||
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
||||||
])
|
])
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
@@ -2142,6 +2179,7 @@ mod tests {
|
|||||||
build_object_refs(&[
|
build_object_refs(&[
|
||||||
(b"objects.rlb".as_slice(), b"parent_proto".as_slice()),
|
(b"objects.rlb".as_slice(), b"parent_proto".as_slice()),
|
||||||
(b"fortif.rlb".as_slice(), b"child_proto.bas".as_slice()),
|
(b"fortif.rlb".as_slice(), b"child_proto.bas".as_slice()),
|
||||||
|
(b"fortif.rlb".as_slice(), b"parent_proto.cpt".as_slice()),
|
||||||
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
(b"fortif.rlb".as_slice(), b"parent_proto.ctl".as_slice()),
|
||||||
])
|
])
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
@@ -2162,6 +2200,7 @@ mod tests {
|
|||||||
build_nres(&[
|
build_nres(&[
|
||||||
(b"child_proto.bas".as_slice(), b"base"),
|
(b"child_proto.bas".as_slice(), b"base"),
|
||||||
(b"parent_proto.ctl".as_slice(), b"control"),
|
(b"parent_proto.ctl".as_slice(), b"control"),
|
||||||
|
(b"parent_proto.cpt".as_slice(), b"points"),
|
||||||
])
|
])
|
||||||
.into_boxed_slice(),
|
.into_boxed_slice(),
|
||||||
),
|
),
|
||||||
@@ -2181,6 +2220,9 @@ mod tests {
|
|||||||
let control = resolved.control_resource.expect("inherited explicit CTL");
|
let control = resolved.control_resource.expect("inherited explicit CTL");
|
||||||
assert_eq!(control.archive.as_str(), "fortif.rlb");
|
assert_eq!(control.archive.as_str(), "fortif.rlb");
|
||||||
assert!(control.name.0.eq_ignore_ascii_case(b"parent_proto.ctl"));
|
assert!(control.name.0.eq_ignore_ascii_case(b"parent_proto.ctl"));
|
||||||
|
let points = resolved.point_resource.expect("inherited explicit CPT");
|
||||||
|
assert_eq!(points.archive.as_str(), "fortif.rlb");
|
||||||
|
assert!(points.name.0.eq_ignore_ascii_case(b"parent_proto.cpt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2200,6 +2242,10 @@ mod tests {
|
|||||||
b"missing.rlb".as_slice(),
|
b"missing.rlb".as_slice(),
|
||||||
b"optional_control.ctl".as_slice(),
|
b"optional_control.ctl".as_slice(),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
b"explicit_points.rlb".as_slice(),
|
||||||
|
b"other_points.cpt".as_slice(),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
)])
|
)])
|
||||||
@@ -2230,6 +2276,11 @@ mod tests {
|
|||||||
assert_eq!(control.archive.as_str(), "missing.rlb");
|
assert_eq!(control.archive.as_str(), "missing.rlb");
|
||||||
assert_eq!(control.name, resource_name(b"optional_control.ctl"));
|
assert_eq!(control.name, resource_name(b"optional_control.ctl"));
|
||||||
assert_eq!(control.type_id, None);
|
assert_eq!(control.type_id, None);
|
||||||
|
let points = resolved
|
||||||
|
.point_resource
|
||||||
|
.expect("preserve independent explicit CPT reference");
|
||||||
|
assert_eq!(points.archive.as_str(), "explicit_points.rlb");
|
||||||
|
assert_eq!(points.name, resource_name(b"other_points.cpt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+38
-11
@@ -414,17 +414,44 @@ sentinel `-1`. Повторные node ID сохраняют исходный п
|
|||||||
runtime animation path. Остальные
|
runtime animation path. Остальные
|
||||||
секции CTLD shape records и contact solver не входят в эту интерпретацию.
|
секции CTLD shape records и contact solver не входят в эту интерпретацию.
|
||||||
|
|
||||||
Asset reader traverses common typed-record framing and preserves supported
|
Читатель ресурсов проходит общую структуру typed records и сохраняет строки
|
||||||
type-4 effect rows as data: table and row indices, raw condition words, selector,
|
эффектов type 4 как данные: индексы таблицы и строки, raw condition words,
|
||||||
resource pair, and three offsets into the component-local CPT list. An unknown
|
selector, пару ресурсов и три offsets в point list компонента. Неизвестный tag
|
||||||
typed tag or unsupported layout sets the effect section to
|
или неподдерживаемый layout задаёт `PreparedControlEffectSection::Unsupported`,
|
||||||
`PreparedControlEffectSection::Unsupported`, сохраняя уже разобранные pose
|
сохраняя уже разобранные pose bindings; повреждённая распознанная структура
|
||||||
bindings; malformed распознанная структура отклоняет подготовку ресурса. В
|
отклоняет подготовку ресурса. В GOG corpus `fr_l_plant.ctl` содержит 38 строк
|
||||||
read-only GOG corpus pass `fr_l_plant.ctl` дал 38 type-4 rows, включая четыре
|
type 4, в том числе четыре `f_smalllight_y` с offsets 104–107 и `smoke_fr_02` с
|
||||||
`f_smalllight_y` rows с offsets 104–107 и `smoke_fr_02` с offsets 119–121;
|
offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, включая
|
||||||
`fr_l_gener.ctl` дал 35 rows, включая `f_gener_ball` с offset 85. Это только
|
`f_gener_ball` с offset 85.
|
||||||
разбор ресурсов: разрешение CPT, оценка условий, activation, выполнение FX
|
|
||||||
opcodes и видимый рендер эффектов здесь не реализованы.
|
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
|
||||||
|
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
|
||||||
|
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT
|
||||||
|
сначала идёт `u32` count, затем `count` записей по 36 байт и параллельные им
|
||||||
|
имена по 32 байта. Подготовка проверяет count, точную длину и конечность шести
|
||||||
|
float-полей. Она сохраняет flags, знаковые слова по смещениям `+4` и `+8`, два
|
||||||
|
набора из трёх float и raw name. `Control+0xB210` копирует слово `+4` в
|
||||||
|
производную запись точки, а `Control+0x1B4F0` передаёт это значение callback-у
|
||||||
|
выбора model node; роль слова `+8` и координатный frame float-полей пока не
|
||||||
|
назначены.
|
||||||
|
|
||||||
|
Native lookup выбирает первую группу CPT с тем же C0, а type-4 path прибавляет
|
||||||
|
к её началу row offset. Для корневых компонентов `fr_l_plant` и `fr_l_gener`
|
||||||
|
C0 равен 0, поэтому найденные offsets 104–107, 119–121 и 75–82, 85 указывают на
|
||||||
|
соответствующие записи в их CPT-списках. Общий путь выбора группы для других C0
|
||||||
|
и семантика sentinel `u32::MAX` пока не реализованы; reader сохраняет offsets
|
||||||
|
как raw значения. Отсутствующая явная CPT оставляет `point_list` отсутствующим,
|
||||||
|
но не мешает исследовать строки эффектов. Это только чтение ресурсов: проверка
|
||||||
|
условий, загрузка FX materials, activation, выполнение FX opcodes и видимый
|
||||||
|
рендер эффектов пока не реализованы.
|
||||||
|
|
||||||
|
CPU-only обход `MISSIONS/Autodemo.00/data.tma` подготовил четыре CTL с 80
|
||||||
|
строками type 4 и четыре явных CPT-списка (всего 322 записи). Среди пар были
|
||||||
|
`fortif.rlb/fr_l_plant.ctl` и `fortif.rlb/fr_l_plant.cpt` (155 точек), а также
|
||||||
|
`parts.rlb/u_all_def_a_01.ctl` и `parts.rlb/u_all_def_a_01.cpt` (6 точек). Среди
|
||||||
|
240 raw offset words не встретился `u32::MAX`; наибольшее значение — 121. Это
|
||||||
|
наблюдение ограничено указанной выборкой и не определяет sentinel для других
|
||||||
|
ресурсов.
|
||||||
|
|
||||||
### Terrain queries и movement handoff
|
### Terrain queries и movement handoff
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user