1use 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
18impl From<Inventory> for Modifier {
21 fn from(value: Inventory) -> Self {
22 Modifier::Inventory(value)
23 }
24}
25
26#[derive(Clone, Debug, Eq, Hash, PartialEq)]
35#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
36pub struct InvInBlock {
37 pub(crate) size: Ix,
39
40 pub(crate) icon_scale: Resolution,
43
44 pub(crate) icon_resolution: Resolution,
50
51 pub(crate) icon_rows: Vec<IconRow>,
56}
57
58#[derive(Clone, Eq, Hash, PartialEq)]
60pub struct IconRow {
61 pub(crate) first_slot: Ix,
63 pub(crate) count: Ix,
64 pub(crate) origin: GridPoint,
65 pub(crate) stride: GridVector,
66}
67
68impl InvInBlock {
71 pub const EMPTY: Self = Self {
73 size: 0,
74 icon_scale: Resolution::R1, icon_resolution: Resolution::R1, icon_rows: Vec::new(),
77 };
78
79 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 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 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 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 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 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 #[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 fn rotate(self, transform: Gridgid, icon_size: GridCoordinate) -> Option<Self> {
232 Some(Self {
235 first_slot: self.first_slot,
236 count: self.count,
237
238 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#[cfg(feature = "arbitrary")]
272impl<'a> arbitrary::Arbitrary<'a> for IconRow {
273 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
274 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#[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 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 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}