Skip to main content

rpfm_lib/files/group_formations/
layout.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Simulated layout of the blocks of a formation.
12//!
13//! The real size of a block depends on the units the game assigns to it, so this module simulates
14//! a deployment with a configurable number of units per block. Distances are in meters, and +Y is
15//! the front of the formation.
16//!
17//! NOTE: how the game applies relative offsets hasn't been confirmed in-game. This module assumes an
18//! offset is a gap measured from the parent's edge, in the offset's direction, and that a zero offset
19//! on an axis centers the block on its parent. If it turns out offsets are center-to-center, only
20//! [`place_relative`] and [`offset_for_center`] need to change.
21
22use getset::*;
23
24use std::collections::{HashMap, HashSet};
25
26use super::{Block, EntityArrangement, GroupFormation, GroupFormationBlock};
27
28//---------------------------------------------------------------------------//
29//                              Enum & Structs
30//---------------------------------------------------------------------------//
31
32/// Area covered by a block in the simulated layout.
33#[derive(Clone, Copy, Debug, Default, PartialEq, CopyGetters)]
34#[getset(get_copy = "pub")]
35pub struct LayoutRect {
36
37    /// X coordinate of the center of the block.
38    center_x: f32,
39
40    /// Y coordinate of the center of the block.
41    center_y: f32,
42
43    /// Size of the block across the formation's front.
44    width: f32,
45
46    /// Size of the block along the formation's depth.
47    height: f32,
48}
49
50/// Parameters of the simulated deployment used to compute a layout.
51#[derive(Clone, Debug, PartialEq, Getters, MutGetters, Setters)]
52#[getset(get = "pub", get_mut = "pub", set = "pub")]
53pub struct LayoutParams {
54
55    /// Frontage of a single unit.
56    unit_width: f32,
57
58    /// Depth of a single unit.
59    unit_depth: f32,
60
61    /// Units assumed on each block without an explicit count, before applying the block's thresholds.
62    default_unit_count: u32,
63
64    /// Explicit unit counts for specific blocks, by block id.
65    unit_counts: HashMap<u32, u32>,
66}
67
68/// Places blocks recursively, caching the results.
69struct LayoutBuilder<'a> {
70    blocks: HashMap<u32, &'a GroupFormationBlock>,
71    params: &'a LayoutParams,
72    rects: HashMap<u32, LayoutRect>,
73    visiting: HashSet<u32>,
74    unplaceable: HashSet<u32>,
75}
76
77//---------------------------------------------------------------------------//
78//                             Implementations
79//---------------------------------------------------------------------------//
80
81impl Default for LayoutParams {
82    fn default() -> Self {
83        Self {
84            unit_width: 20.0,
85            unit_depth: 8.0,
86            default_unit_count: 2,
87            unit_counts: HashMap::new(),
88        }
89    }
90}
91
92impl LayoutRect {
93
94    /// Creates a rect from its center and size.
95    pub fn new(center_x: f32, center_y: f32, width: f32, height: f32) -> Self {
96        Self { center_x, center_y, width, height }
97    }
98
99    /// X coordinate of the left edge.
100    pub fn left(&self) -> f32 {
101        self.center_x - self.width / 2.0
102    }
103
104    /// X coordinate of the right edge.
105    pub fn right(&self) -> f32 {
106        self.center_x + self.width / 2.0
107    }
108
109    /// Y coordinate of the front edge.
110    pub fn front(&self) -> f32 {
111        self.center_y + self.height / 2.0
112    }
113
114    /// Y coordinate of the back edge.
115    pub fn back(&self) -> f32 {
116        self.center_y - self.height / 2.0
117    }
118
119    /// Returns the smallest rect containing both rects.
120    pub fn union(&self, other: &Self) -> Self {
121        let left = self.left().min(other.left());
122        let right = self.right().max(other.right());
123        let back = self.back().min(other.back());
124        let front = self.front().max(other.front());
125        Self::new((left + right) / 2.0, (back + front) / 2.0, right - left, front - back)
126    }
127}
128
129impl GroupFormation {
130
131    /// Computes the simulated position and size of every block of the formation.
132    ///
133    /// # Arguments
134    ///
135    /// * `params` - The simulated deployment to use.
136    ///
137    /// # Returns
138    ///
139    /// The rect of each block, by block id. Blocks that can't be placed due to missing references,
140    /// reference cycles or empty spans are left out.
141    pub fn layout(&self, params: &LayoutParams) -> HashMap<u32, LayoutRect> {
142        let mut blocks = HashMap::with_capacity(self.group_formation_blocks.len());
143        for block in &self.group_formation_blocks {
144
145            // On duplicated ids, references find the first block, as in validation.
146            blocks.entry(block.block_id).or_insert(block);
147        }
148
149        let mut builder = LayoutBuilder {
150            blocks,
151            params,
152            rects: HashMap::with_capacity(self.group_formation_blocks.len()),
153            visiting: HashSet::new(),
154            unplaceable: HashSet::new(),
155        };
156
157        for block in &self.group_formation_blocks {
158            builder.place(block.block_id);
159        }
160
161        builder.rects
162    }
163}
164
165impl LayoutBuilder<'_> {
166
167    /// Returns the rect of a block, placing it and its dependencies if they weren't placed yet.
168    fn place(&mut self, block_id: u32) -> Option<LayoutRect> {
169        if let Some(rect) = self.rects.get(&block_id) {
170            return Some(*rect);
171        }
172
173        // A block being visited again means a reference cycle.
174        if self.unplaceable.contains(&block_id) || !self.visiting.insert(block_id) {
175            return None;
176        }
177
178        let block = self.blocks.get(&block_id).copied();
179        let rect = block.and_then(|block| match &block.block {
180            Block::ContainerAbsolute(container) => {
181                let (width, height) = self.footprint(block)?;
182                Some(LayoutRect::new(*container.position_x(), *container.position_y(), width, height))
183            },
184            Block::ContainerRelative(container) => {
185                let parent = self.place(*container.relative_block_id())?;
186                let (width, height) = self.footprint(block)?;
187                Some(place_relative(&parent, width, height, *container.position_x(), *container.position_y()))
188            },
189            Block::Spanning(span) => span.spanned_block_ids()
190                .iter()
191                .map(|member_id| self.place(*member_id))
192                .collect::<Option<Vec<_>>>()?
193                .into_iter()
194                .reduce(|total, rect| total.union(&rect)),
195        });
196
197        self.visiting.remove(&block_id);
198        match rect {
199            Some(rect) => { self.rects.insert(block_id, rect); },
200            None => { self.unplaceable.insert(block_id); },
201        }
202
203        rect
204    }
205
206    /// Returns the width and height of a container with its simulated unit count, or `None` for spans.
207    fn footprint(&self, block: &GroupFormationBlock) -> Option<(f32, f32)> {
208        let (arrangement, spacing, crescent_y_offset, minimum, maximum) = match &block.block {
209            Block::ContainerAbsolute(container) => (container.entity_arrangement, container.inter_entity_spacing, container.crescent_y_offset, container.minimum_entity_threshold, container.maximum_entity_threshold),
210            Block::ContainerRelative(container) => (container.entity_arrangement, container.inter_entity_spacing, container.crescent_y_offset, container.minimum_entity_threshold, container.maximum_entity_threshold),
211            Block::Spanning(_) => return None,
212        };
213
214        let count = self.params.unit_counts.get(&block.block_id).copied().unwrap_or(self.params.default_unit_count) as i32;
215
216        // A negative maximum means there's no limit. Empty blocks still get one unit so they're visible.
217        let count = if maximum >= 0 { count.min(maximum) } else { count };
218        let count = count.max(minimum).max(1) as f32;
219
220        let width = count * self.params.unit_width + (count - 1.0) * spacing;
221        let depth = count * self.params.unit_depth + (count - 1.0) * spacing;
222        match arrangement {
223            EntityArrangement::Line => Some((width, self.params.unit_depth)),
224
225            // Crescents are lines bent by their offset, so they're deeper by it.
226            EntityArrangement::CrescentFront |
227            EntityArrangement::CrescentBack => Some((width, self.params.unit_depth + crescent_y_offset.abs())),
228            EntityArrangement::Column => Some((self.params.unit_width, depth)),
229        }
230    }
231}
232
233/// Places a block relative to its parent.
234///
235/// # Arguments
236///
237/// * `parent` - Rect of the parent block.
238/// * `width` - Width of the block being placed.
239/// * `height` - Height of the block being placed.
240/// * `offset_x` - Gap to the parent's right edge if positive, to its left edge if negative, or 0 to center it.
241/// * `offset_y` - Gap to the parent's front edge if positive, to its back edge if negative, or 0 to center it.
242///
243/// # Returns
244///
245/// The rect of the placed block.
246pub fn place_relative(parent: &LayoutRect, width: f32, height: f32, offset_x: f32, offset_y: f32) -> LayoutRect {
247    let center_x = if offset_x > 0.0 {
248        parent.right() + offset_x + width / 2.0
249    } else if offset_x < 0.0 {
250        parent.left() + offset_x - width / 2.0
251    } else {
252        parent.center_x
253    };
254
255    let center_y = if offset_y > 0.0 {
256        parent.front() + offset_y + height / 2.0
257    } else if offset_y < 0.0 {
258        parent.back() + offset_y - height / 2.0
259    } else {
260        parent.center_y
261    };
262
263    LayoutRect::new(center_x, center_y, width, height)
264}
265
266/// Computes the offset that places a block closest to a target position, as the inverse of [`place_relative`].
267///
268/// # Arguments
269///
270/// * `parent` - Rect of the parent block.
271/// * `width` - Width of the block being placed.
272/// * `height` - Height of the block being placed.
273/// * `center_x` - Target X coordinate of the block's center.
274/// * `center_y` - Target Y coordinate of the block's center.
275///
276/// # Returns
277///
278/// The X and Y offsets. Axes on which the target overlaps the parent get a 0 offset, centering the block on the parent.
279pub fn offset_for_center(parent: &LayoutRect, width: f32, height: f32, center_x: f32, center_y: f32) -> (f32, f32) {
280    let left = center_x - width / 2.0;
281    let right = center_x + width / 2.0;
282    let offset_x = if left > parent.right() {
283        left - parent.right()
284    } else if right < parent.left() {
285        right - parent.left()
286    } else {
287        0.0
288    };
289
290    let back = center_y - height / 2.0;
291    let front = center_y + height / 2.0;
292    let offset_y = if back > parent.front() {
293        back - parent.front()
294    } else if front < parent.back() {
295        front - parent.back()
296    } else {
297        0.0
298    };
299
300    (offset_x, offset_y)
301}