all_is_cubes/inv/
inv_in_block.rs

1//! Configuration of inventories owned by blocks ([`Modifier::Inventory`]).
2
3use core::fmt;
4
5use alloc::vec::Vec;
6
7use euclid::Point3D;
8use manyfmt::Refmt;
9
10use crate::block::{Modifier, Resolution};
11use crate::inv::{Inventory, Ix};
12use crate::math::{GridCoordinate, GridPoint, GridRotation, GridVector, Gridgid};
13use crate::util::ConciseDebug;
14
15#[cfg(doc)]
16use crate::block::{self, Block};
17
18// -------------------------------------------------------------------------------------------------
19
20impl From<Inventory> for Modifier {
21    fn from(value: Inventory) -> Self {
22        Modifier::Inventory(value)
23    }
24}
25
26// -------------------------------------------------------------------------------------------------
27
28/// Defines how a [`Modifier::Inventory`] should be configured and displayed within a [`Block`].
29///
30/// Attach this to a block using [`block::Builder::inventory_config()`].
31//---
32// TODO(inventory): better name?
33// TODO(inventory): needs accessors or public fields
34#[derive(Clone, Debug, Eq, Hash, PartialEq)]
35#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
36pub struct InvInBlock {
37    /// Number of slots the inventory should have.
38    pub(crate) size: Ix,
39
40    /// Scale factor by which to scale down the inventory icon blocks,
41    /// relative to the bounds of the block in which they are being displayed.
42    pub(crate) icon_scale: Resolution,
43
44    /// Maximum resolution of inventory icons, and resolution in which the `icon_rows`
45    /// position coordinatess are expressed.
46    ///
47    /// [`Modifier::Inventory`] is guaranteed not to increase the block resolution
48    /// beyond this resolution.
49    pub(crate) icon_resolution: Resolution,
50
51    // TODO: following the rule for all `BlockAttributes` being cheap to clone,
52    // this should be `Arc<[IconRow]>`, but that's mildly inconvenient for `Arbitrary`, so
53    // I'm not bothering for this first iteration.
54    // (But maybe we should be handling that at a higher level of the structure.)
55    pub(crate) icon_rows: Vec<IconRow>,
56}
57
58/// Positioning of a displayed row of inventory icons; part of [`InvInBlock`].
59#[derive(Clone, Eq, Hash, PartialEq)]
60pub struct IconRow {
61    // visible for serialization -- TODO: improve on that
62    pub(crate) first_slot: Ix,
63    pub(crate) count: Ix,
64    pub(crate) origin: GridPoint,
65    pub(crate) stride: GridVector,
66}
67
68// -------------------------------------------------------------------------------------------------
69
70impl InvInBlock {
71    /// Value appropriate for “normal” blocks which should not carry inventories.
72    pub const EMPTY: Self = Self {
73        size: 0,
74        icon_scale: Resolution::R1,      // arbitrary
75        icon_resolution: Resolution::R1, // arbitrary
76        icon_rows: Vec::new(),
77    };
78
79    /// Constructs a [`InvInBlock`].
80    ///
81    /// * `inventory_size` is the number of slots the inventory should have.
82    /// * `icon_scale` is the scale factor by which to scale down the inventory icon blocks,
83    ///   relative to the bounds of the block in which they are being displayed.
84    /// * `icon_resolution` is the maximum resolution of inventory icons, and resolution in which
85    ///   the `icon_rows`’ position coordinatess are expressed.
86    ///   [`Modifier::Inventory`] is guaranteed not to increase the block resolution
87    ///   beyond this resolution.
88    /// * `icon_rows` specifies where in the block the inventory icons should be displayed.
89    ///   It may be empty in order to keep the inventory invisible.
90    pub fn new(
91        inventory_size: Ix,
92        icon_scale: Resolution,
93        icon_resolution: Resolution,
94        icon_rows: impl IntoIterator<Item = IconRow>,
95    ) -> Self {
96        Self {
97            size: inventory_size,
98            icon_scale,
99            icon_resolution,
100            icon_rows: icon_rows.into_iter().collect(),
101        }
102    }
103
104    /// Returns which inventory slots should be rendered as icons, and the lower corners
105    /// of the icons.
106    ///
107    /// `inventory_size` should be the size of the inventory to be rendered, which will be used
108    /// to filter out nonexistent slots and limit the amount of computation performed to match
109    /// the inventory.
110    pub(crate) fn icon_positions(
111        &self,
112        inventory_size: Ix,
113    ) -> impl Iterator<Item = (Ix, GridPoint)> + '_ {
114        self.icon_rows.iter().flat_map(move |row| {
115            (0..row.count).map_while(move |sub_index| {
116                let slot_index = row.first_slot.checked_add(sub_index)?;
117                if slot_index >= inventory_size {
118                    return None;
119                }
120                let index_coord = GridCoordinate::from(sub_index);
121                let position: GridPoint = transpose_point_option(
122                    row.origin
123                        .to_vector()
124                        .zip(row.stride, |origin_c, stride_c| {
125                            origin_c.checked_add(stride_c.checked_mul(index_coord)?)
126                        })
127                        .to_point(),
128                )?;
129                Some((slot_index, position))
130            })
131        })
132    }
133
134    pub(crate) fn rotationally_symmetric(&self) -> bool {
135        // If it doesn't display any icons, then it's symmetric.
136        self.icon_rows.is_empty()
137    }
138
139    pub(crate) fn rotate(self, rotation: GridRotation) -> Self {
140        let Self {
141            size,
142            icon_scale,
143            icon_resolution,
144            icon_rows,
145        } = self;
146        let transform = rotation.to_positive_octant_transform(icon_resolution.into());
147        let icon_size =
148            GridCoordinate::from((icon_resolution / icon_scale).unwrap_or(Resolution::R1));
149        Self {
150            size,
151            icon_scale,
152            icon_resolution,
153            icon_rows: icon_rows
154                .into_iter()
155                .filter_map(|row| row.rotate(transform, icon_size))
156                .collect(),
157        }
158    }
159
160    /// Combine the two inputs to form one which has the size and display of both.
161    pub(crate) fn concatenate(self, other: InvInBlock) -> InvInBlock {
162        if self.size == 0 {
163            other
164        } else {
165            Self {
166                size: self.size.saturating_add(other.size),
167                // TODO(inventory): scale and resolution need adaptation
168                icon_scale: self.icon_scale,
169                icon_resolution: self.icon_resolution,
170                icon_rows: self
171                    .icon_rows
172                    .into_iter()
173                    .chain(other.icon_rows.into_iter().filter_map(|mut row| {
174                        row.first_slot = row.first_slot.checked_add(self.size)?;
175                        Some(row)
176                    }))
177                    .collect(),
178            }
179        }
180    }
181}
182
183fn transpose_point_option<T, U>(v: Point3D<Option<T>, U>) -> Option<Point3D<T, U>> {
184    Some(Point3D::new(v.x?, v.y?, v.z?))
185}
186
187impl Default for InvInBlock {
188    /// Returns [`InvInBlock::EMPTY`].
189    fn default() -> Self {
190        Self::EMPTY
191    }
192}
193
194impl crate::universe::VisitHandles for InvInBlock {
195    fn visit_handles(&self, _: &mut dyn crate::universe::HandleVisitor) {
196        let Self {
197            size: _,
198            icon_scale: _,
199            icon_resolution: _,
200            icon_rows: _,
201        } = self;
202    }
203}
204
205impl IconRow {
206    /// Constructs an `IconRow`.
207    ///
208    /// * `slot_range` is the the portion of the inventory that is displayed.
209    /// * `origin` is the lower corner of the location within the voxels where the first slot
210    ///   in `slot_range` is to be displayed.
211    /// * `stride` is the translation between adjacent slots.
212    ///
213    /// # Panics
214    ///
215    /// Panics if `slot_range` has `end` less than `start`.
216    #[track_caller]
217    pub fn new(slot_range: core::ops::Range<Ix>, origin: GridPoint, stride: GridVector) -> Self {
218        Self {
219            first_slot: slot_range.start,
220            count: slot_range
221                .end
222                .checked_sub(slot_range.start)
223                .expect("slot_range must not be reversed"),
224            origin,
225            stride,
226        }
227    }
228
229    /// Rotate this row.
230    /// Returns [`None`] if rotating it would cause numeric overflow.
231    fn rotate(self, transform: Gridgid, icon_size: GridCoordinate) -> Option<Self> {
232        // TODO: The icons themselves (not only their positions) need to be rotated,
233        // but this is not supported yet.
234        Some(Self {
235            first_slot: self.first_slot,
236            count: self.count,
237
238            // Taking the minimum of opposing corners accounts for which direction the
239            // block extends.
240            origin: transform.checked_transform_point(self.origin)?.min(
241                transform.checked_transform_point(checked_add_point_vector(
242                    self.origin,
243                    GridVector::splat(icon_size),
244                )?)?,
245            ),
246            stride: transform.rotation.checked_transform_vector(self.stride)?,
247        })
248    }
249}
250
251impl fmt::Debug for IconRow {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        let &Self {
254            first_slot,
255            count,
256            origin,
257            stride,
258        } = self;
259        write!(
260            f,
261            "IconRow({slot_range:?} @ {origin} + {stride})",
262            slot_range = (first_slot..(first_slot + count)),
263            origin = origin.refmt(&ConciseDebug),
264            stride = stride.refmt(&ConciseDebug)
265        )
266    }
267}
268
269// Manual implementation of `Arbitrary` because, currently, if we don't then the
270// size hint will be missing, because the `euclid` vector types don't give one.
271#[cfg(feature = "arbitrary")]
272impl<'a> arbitrary::Arbitrary<'a> for IconRow {
273    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
274        // TODO: there are lots of extremely useless out-of-bounds values here;
275        // bias to useful ones?
276        Ok(Self {
277            first_slot: u.arbitrary()?,
278            count: u.arbitrary()?,
279            origin: <[i32; 3]>::arbitrary(u)?.into(),
280            stride: <[i32; 3]>::arbitrary(u)?.into(),
281        })
282    }
283
284    fn size_hint(depth: usize) -> (usize, Option<usize>) {
285        use arbitrary::{Arbitrary, size_hint::and_all};
286        and_all(&[
287            <usize as Arbitrary>::size_hint(depth),
288            <usize as Arbitrary>::size_hint(depth),
289            <[GridCoordinate; 3] as Arbitrary>::size_hint(depth),
290            <[GridCoordinate; 3] as Arbitrary>::size_hint(depth),
291        ])
292    }
293}
294
295fn checked_add_point_vector(p: GridPoint, v: GridVector) -> Option<GridPoint> {
296    Some(GridPoint::new(
297        p.x.checked_add(v.x)?,
298        p.y.checked_add(v.y)?,
299        p.z.checked_add(v.z)?,
300    ))
301}
302
303// -------------------------------------------------------------------------------------------------
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use euclid::{point3, vec3};
309    use pretty_assertions::assert_eq;
310
311    #[test]
312    fn inv_in_block_debug() {
313        let iib = InvInBlock::new(
314            9,
315            Resolution::R4,
316            Resolution::R16,
317            vec![
318                IconRow::new(0..3, point3(1, 1, 1), vec3(5, 0, 0)),
319                IconRow::new(3..6, point3(1, 1, 6), vec3(5, 0, 0)),
320                IconRow::new(6..9, point3(1, 1, 11), vec3(5, 0, 0)),
321            ],
322        );
323
324        assert_eq!(
325            format!("{iib:#?}"),
326            indoc::indoc! {
327                "
328                InvInBlock {
329                    size: 9,
330                    icon_scale: 4,
331                    icon_resolution: 16,
332                    icon_rows: [
333                        IconRow(0..3 @ (+1, +1, +1) + (+5, +0, +0)),
334                        IconRow(3..6 @ (+1, +1, +6) + (+5, +0, +0)),
335                        IconRow(6..9 @ (+1, +1, +11) + (+5, +0, +0)),
336                    ],
337                }"
338            },
339        );
340    }
341
342    #[test]
343    fn icon_positions_output() {
344        let iib = InvInBlock::new(
345            9,
346            Resolution::R4,
347            Resolution::R16,
348            vec![
349                IconRow::new(0..3, point3(1, 1, 1), vec3(5, 0, 0)),
350                IconRow::new(3..6, point3(1, 1, 6), vec3(5, 0, 0)),
351                IconRow::new(6..9, point3(1, 1, 11), vec3(5, 0, 0)),
352            ],
353        );
354        assert_eq!(
355            iib.icon_positions(999).take(100).collect::<Vec<_>>(),
356            vec![
357                (0, point3(1, 1, 1)),
358                (1, point3(6, 1, 1)),
359                (2, point3(11, 1, 1)),
360                (3, point3(1, 1, 6)),
361                (4, point3(6, 1, 6)),
362                (5, point3(11, 1, 6)),
363                (6, point3(1, 1, 11)),
364                (7, point3(6, 1, 11)),
365                (8, point3(11, 1, 11)),
366            ]
367        );
368    }
369
370    #[test]
371    fn icon_positions_are_truncated_to_inventory_size() {
372        let iib = InvInBlock {
373            size: 1,
374            icon_scale: Resolution::R4,
375            icon_resolution: Resolution::R16,
376            icon_rows: vec![
377                IconRow {
378                    first_slot: 0,
379                    count: 100,
380                    origin: GridPoint::new(0, 0, 0),
381                    stride: GridVector::new(5, 0, 0),
382                },
383                IconRow {
384                    first_slot: 1,
385                    count: 100,
386                    origin: GridPoint::new(0, 0, 5),
387                    stride: GridVector::new(5, 0, 0),
388                },
389            ],
390        };
391        assert_eq!(
392            iib.icon_positions(3).take(100).collect::<Vec<_>>(),
393            vec![
394                (0, point3(0, 0, 0)),
395                (1, point3(5, 0, 0)),
396                (2, point3(10, 0, 0)),
397                (1, point3(0, 0, 5)),
398                (2, point3(5, 0, 5)),
399            ]
400        );
401    }
402
403    #[test]
404    fn rotated_positions() {
405        assert_eq!(
406            IconRow {
407                first_slot: 0,
408                count: 10,
409                origin: GridPoint::new(2, 2, 0),
410                stride: GridVector::new(0, 0, 4),
411            }
412            .rotate(GridRotation::Rxyz.to_positive_octant_transform(8), 4),
413            Some(IconRow {
414                first_slot: 0,
415                count: 10,
416                origin: GridPoint::new(2, 2, 4),
417                stride: GridVector::new(0, 0, -4),
418            }),
419        );
420    }
421
422    #[test]
423    fn rotate_row_overflow() {
424        // For coverage, two cases:
425        // 1. case without icon size involved
426        assert_eq!(
427            IconRow {
428                first_slot: 0,
429                count: 10,
430                origin: GridPoint::new(1397969747, -2147483648, 255827),
431                stride: GridVector::new(134767872, 2820644, 7285711),
432            }
433            .rotate(Gridgid::from_rotation_about_origin(GridRotation::Rxyz), 1),
434            None,
435        );
436        // 1. case where icon size causes the overflow
437        assert_eq!(
438            IconRow {
439                first_slot: 0,
440                count: 10,
441                origin: GridPoint::new(1397969747, i32::MAX - 1, 255827),
442                stride: GridVector::new(134767872, 2820644, 7285711),
443            }
444            .rotate(Gridgid::from_rotation_about_origin(GridRotation::Rxyz), 4),
445            None,
446        );
447    }
448}