Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 14 additions & 24 deletions rust_parser/core/src/mapdata/describe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,31 +425,21 @@ pub fn describe_instance(store: &SaveStore, index: &MapIndex, instance_name: &st
}
}

// Mixed-mark pipe network detection (rated members only).
// Mixed-mark pipe detection, scoped to the hovered segment's LINE (walk
// ends at splitting/merging junctions), not the whole FGPipeNetwork --
// a Mk2 trunk legitimately feeding two Mk1 branches through a junction
// is not a bottleneck, matching how conveyor chains end at splitters.
if queries::pipe_flow_limit_per_minute(type_path.as_deref()).is_some() {
for connector_suffix in PIPE_CONNECTOR_SUFFIXES {
connector_key.clear();
connector_key.extend_from_slice(instance_name.as_bytes());
connector_key.extend_from_slice(connector_suffix.as_bytes());
let Some(connector_object) = index.parse_object_by_name(store, &connector_key) else {
continue;
};
let network_id = props::int(&connector_object.properties, data, b"mPipeNetworkID");
let member_names =
network_id.and_then(|id| index.pipe_network_id_to_members.get(&id));
// `if memberNames:` -- the break sits inside this truthy gate.
if let Some(member_names) = member_names {
if !member_names.is_empty() {
if let Some(network_bottleneck) = queries::pipe_network_bottleneck(
store,
index,
member_names,
type_path.as_deref(),
) {
result.insert("lineBottleneck".into(), network_bottleneck);
}
break;
}
let member_names =
queries::pipe_line_member_names(store, index, instance_name.as_bytes());
if !member_names.is_empty() {
if let Some(line_bottleneck) = queries::pipe_network_bottleneck(
store,
index,
&member_names,
type_path.as_deref(),
) {
result.insert("lineBottleneck".into(), line_bottleneck);
}
}
}
Expand Down
73 changes: 71 additions & 2 deletions rust_parser/core/src/mapdata/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,76 @@ pub fn conveyor_chain_bottleneck(
)
}

/// sav_map_data._pipeNetworkBottleneck.
/// Rated members of the hovered segment's pipe LINE (not the whole
/// FGPipeNetwork): walks mConnectedComponent links outward from `start`,
/// continuing through rated pipes/pumps and through junctions acting as
/// plain couplings (<= 2 connected ports), and stopping at junctions that
/// actually split or merge the flow (>= 3 connected ports) and at anything
/// unrated (machines, valves, tanks). Mirrors conveyor bottleneck scoping,
/// where the game's chain actors end at splitters/mergers -- a Mk2 feeding
/// two Mk1 branches through a junction is three separate lines, not one
/// mixed-mark network.
pub fn pipe_line_member_names(
store: &SaveStore,
index: &MapIndex,
start_instance_name: &[u8],
) -> Vec<String> {
let data: &[u8] = &store.data;
// Connected peer OWNER instances of one member (component objects are
// named <owner>.<Connection*>; a port without a peer has no
// mConnectedComponent).
let connected_peers = |instance: &[u8]| -> Vec<Vec<u8>> {
let mut peers = Vec::new();
let mut key: Vec<u8> = Vec::new();
for suffix in PIPE_CONNECTOR_SUFFIXES {
key.clear();
key.extend_from_slice(instance);
key.extend_from_slice(suffix.as_bytes());
let Some(component) = index.parse_object_by_name(store, &key) else { continue };
let Some(peer) = props::object_ref(&component.properties, data, b"mConnectedComponent")
else {
continue;
};
let peer_path = peer.path_name.bytes(data);
if peer_path.is_empty() {
continue;
}
if let Some(dot) = peer_path.iter().rposition(|&b| b == b'.') {
peers.push(peer_path[..dot].to_vec());
}
}
peers
};

let mut members: Vec<String> = Vec::new();
let mut visited: HashSet<Vec<u8>> = HashSet::new();
let mut queue: Vec<Vec<u8>> = vec![start_instance_name.to_vec()];
while let Some(instance) = queue.pop() {
if !visited.insert(instance.clone()) {
continue;
}
let type_path = match index.header_by_name(store, &instance) {
Some(Header::Actor(a)) => a.type_path.to_string(data),
_ => continue,
};
if pipe_flow_limit_per_minute(Some(&type_path)).is_some() {
members.push(props::lossy(&instance));
queue.extend(connected_peers(&instance));
} else if short_class_name(&type_path).contains("PipelineJunction") {
let peers = connected_peers(&instance);
if peers.len() <= 2 {
queue.extend(peers); // plain coupling: pass through
}
// >= 3 connected ports: the line ends here, like a belt
// chain ends at a splitter/merger.
}
// Anything else (machine, valve, tank, ...) terminates the line.
}
members
}

/// sav_map_data._pipeNetworkBottleneck, rescoped from the whole FGPipeNetwork
/// to the hovered segment's line (see pipe_line_member_names).
pub fn pipe_network_bottleneck(
store: &SaveStore,
index: &MapIndex,
Expand All @@ -402,7 +471,7 @@ pub fn pipe_network_bottleneck(
&member_names,
hovered_type_path,
&pipe_flow_limit_per_minute,
"network",
"line",
"m³/min",
)
}
Expand Down
Loading