1use std::{cmp, num::NonZeroU32, sync::LazyLock};
2
3use azalea_core::{
4 direction::{Axis, AxisCycle, Direction},
5 hit_result::BlockHitResult,
6 math::{EPSILON, binary_search},
7 position::{BlockPos, Vec3},
8};
9
10use super::mergers::IndexMerger;
11use crate::collision::{AABB, BitSetDiscreteVoxelShape, DiscreteVoxelShape};
12
13pub struct Shapes;
14
15pub static BLOCK_SHAPE: LazyLock<VoxelShape> = LazyLock::new(|| {
16 let mut shape = BitSetDiscreteVoxelShape::new(1, 1, 1);
17 shape.fill(0, 0, 0);
18 VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(shape)))
19});
20
21pub fn box_shape(
22 min_x: f64,
23 min_y: f64,
24 min_z: f64,
25 max_x: f64,
26 max_y: f64,
27 max_z: f64,
28) -> VoxelShape {
29 if max_x - min_x < EPSILON && max_y - min_y < EPSILON && max_z - min_z < EPSILON {
33 return EMPTY_SHAPE.clone();
34 }
35
36 let x_bits = find_bits(min_x, max_x);
37 let y_bits = find_bits(min_y, max_y);
38 let z_bits = find_bits(min_z, max_z);
39
40 if x_bits < 0 || y_bits < 0 || z_bits < 0 {
41 return VoxelShape::Array(ArrayVoxelShape::new(
42 BLOCK_SHAPE.shape().to_owned(),
43 vec![min_x, max_x],
44 vec![min_y, max_y],
45 vec![min_z, max_z],
46 ));
47 }
48 if x_bits == 0 && y_bits == 0 && z_bits == 0 {
49 return BLOCK_SHAPE.clone();
50 }
51
52 let x_bits = 1 << x_bits;
53 let y_bits = 1 << y_bits;
54 let z_bits = 1 << z_bits;
55 let shape = BitSetDiscreteVoxelShape::with_filled_bounds(
56 x_bits,
57 y_bits,
58 z_bits,
59 (min_x * x_bits as f64).round() as i32,
60 (min_y * y_bits as f64).round() as i32,
61 (min_z * z_bits as f64).round() as i32,
62 (max_x * x_bits as f64).round() as i32,
63 (max_y * y_bits as f64).round() as i32,
64 (max_z * z_bits as f64).round() as i32,
65 );
66 VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(shape)))
67}
68
69pub static EMPTY_SHAPE: LazyLock<VoxelShape> = LazyLock::new(|| {
70 VoxelShape::Array(ArrayVoxelShape::new(
71 DiscreteVoxelShape::BitSet(BitSetDiscreteVoxelShape::new(0, 0, 0)),
72 vec![0.],
73 vec![0.],
74 vec![0.],
75 ))
76});
77
78fn find_bits(min: f64, max: f64) -> i32 {
79 if min < -EPSILON || max > 1. + EPSILON {
80 return -1;
81 }
82 for bits in 0..=3 {
83 let shifted_bits = 1 << bits;
84 let min = min * shifted_bits as f64;
85 let max = max * shifted_bits as f64;
86 let min_ok = (min - min.round()).abs() < EPSILON * shifted_bits as f64;
87 let max_ok = (max - max.round()).abs() < EPSILON * shifted_bits as f64;
88 if min_ok && max_ok {
89 return bits;
90 }
91 }
92 -1
93}
94
95impl Shapes {
96 pub fn or(a: VoxelShape, b: VoxelShape) -> VoxelShape {
97 Self::join(a, b, |a, b| a || b)
98 }
99
100 pub fn collide(
101 axis: &Axis,
102 entity_box: &AABB,
103 collision_boxes: &Vec<VoxelShape>,
104 mut movement: f64,
105 ) -> f64 {
106 for shape in collision_boxes {
107 if movement.abs() < EPSILON {
108 return 0.;
109 }
110 movement = shape.collide(axis, entity_box, movement);
111 }
112 movement
113 }
114
115 pub fn join(a: VoxelShape, b: VoxelShape, op: fn(bool, bool) -> bool) -> VoxelShape {
116 Self::join_unoptimized(a, b, op).optimize()
117 }
118
119 pub fn join_unoptimized(
120 a: VoxelShape,
121 b: VoxelShape,
122 op: fn(bool, bool) -> bool,
123 ) -> VoxelShape {
124 if op(false, false) {
125 panic!("Illegal operation");
126 };
127 let op_true_false = op(true, false);
131 let op_false_true = op(false, true);
132 if a.is_empty() {
133 return if op_false_true {
134 b
135 } else {
136 EMPTY_SHAPE.clone()
137 };
138 }
139 if b.is_empty() {
140 return if op_true_false {
141 a
142 } else {
143 EMPTY_SHAPE.clone()
144 };
145 }
146 let var5 = Self::create_index_merger(
158 1,
159 a.get_coords(Axis::X),
160 b.get_coords(Axis::X),
161 op_true_false,
162 op_false_true,
163 );
164 let var6 = Self::create_index_merger(
165 (var5.size() - 1).try_into().unwrap(),
166 a.get_coords(Axis::Y),
167 b.get_coords(Axis::Y),
168 op_true_false,
169 op_false_true,
170 );
171 let var7 = Self::create_index_merger(
172 ((var5.size() - 1) * (var6.size() - 1)).try_into().unwrap(),
173 a.get_coords(Axis::Z),
174 b.get_coords(Axis::Z),
175 op_true_false,
176 op_false_true,
177 );
178 let var8 = BitSetDiscreteVoxelShape::join(a.shape(), b.shape(), &var5, &var6, &var7, op);
179 if matches!(var5, IndexMerger::DiscreteCube { .. })
182 && matches!(var6, IndexMerger::DiscreteCube { .. })
183 && matches!(var7, IndexMerger::DiscreteCube { .. })
184 {
185 VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(var8)))
186 } else {
187 VoxelShape::Array(ArrayVoxelShape::new(
188 DiscreteVoxelShape::BitSet(var8),
189 var5.get_list(),
190 var6.get_list(),
191 var7.get_list(),
192 ))
193 }
194 }
195
196 pub fn matches_anywhere(
199 a: &VoxelShape,
200 b: &VoxelShape,
201 op: impl Fn(bool, bool) -> bool,
202 ) -> bool {
203 debug_assert!(!op(false, false));
204 let a_is_empty = a.is_empty();
205 let b_is_empty = b.is_empty();
206 if a_is_empty || b_is_empty {
207 return op(!a_is_empty, !b_is_empty);
208 }
209 if a == b {
210 return op(true, true);
211 }
212
213 let op_true_false = op(true, false);
214 let op_false_true = op(false, true);
215
216 for axis in [Axis::X, Axis::Y, Axis::Z] {
217 if a.max(axis) < b.min(axis) - EPSILON {
218 return op_true_false || op_false_true;
219 }
220 if b.max(axis) < a.min(axis) - EPSILON {
221 return op_true_false || op_false_true;
222 }
223 }
224
225 let x_merger = Self::create_index_merger(
226 1,
227 a.get_coords(Axis::X),
228 b.get_coords(Axis::X),
229 op_true_false,
230 op_false_true,
231 );
232 let y_merger = Self::create_index_merger(
233 (x_merger.size() - 1) as i32,
234 a.get_coords(Axis::Y),
235 b.get_coords(Axis::Y),
236 op_true_false,
237 op_false_true,
238 );
239 let z_merger = Self::create_index_merger(
240 ((x_merger.size() - 1) * (y_merger.size() - 1)) as i32,
241 a.get_coords(Axis::Z),
242 b.get_coords(Axis::Z),
243 op_true_false,
244 op_false_true,
245 );
246
247 Self::matches_anywhere_with_mergers(
248 x_merger,
249 y_merger,
250 z_merger,
251 a.shape().to_owned(),
252 b.shape().to_owned(),
253 op,
254 )
255 }
256
257 pub fn matches_anywhere_with_mergers(
258 merged_x: IndexMerger,
259 merged_y: IndexMerger,
260 merged_z: IndexMerger,
261 shape1: DiscreteVoxelShape,
262 shape2: DiscreteVoxelShape,
263 op: impl Fn(bool, bool) -> bool,
264 ) -> bool {
265 !merged_x.for_merged_indexes(|var5x, var6, _var7| {
266 merged_y.for_merged_indexes(|var6x, var7x, _var8| {
267 merged_z.for_merged_indexes(|var7, var8x, _var9| {
268 !op(
269 shape1.is_full_wide(var5x, var6x, var7),
270 shape2.is_full_wide(var6, var7x, var8x),
271 )
272 })
273 })
274 })
275 }
276
277 pub fn create_index_merger(
278 _var0: i32,
279 coords1: &[f64],
280 coords2: &[f64],
281 var3: bool,
282 var4: bool,
283 ) -> IndexMerger {
284 let var5 = coords1.len() - 1;
285 let var6 = coords2.len() - 1;
286 if coords1[var5] < coords2[0] - EPSILON {
297 IndexMerger::NonOverlapping {
298 lower: coords1.to_vec(),
299 upper: coords2.to_vec(),
300 swap: false,
301 }
302 } else if coords2[var6] < coords1[0] - EPSILON {
303 IndexMerger::NonOverlapping {
304 lower: coords2.to_vec(),
305 upper: coords1.to_vec(),
306 swap: true,
307 }
308 } else if var5 == var6 && coords1 == coords2 {
309 IndexMerger::Identical {
310 coords: coords1.to_vec(),
311 }
312 } else {
313 IndexMerger::new_indirect(coords1, coords2, var3, var4)
314 }
315 }
316}
317
318#[derive(Clone, PartialEq, Debug)]
319pub enum VoxelShape {
320 Array(ArrayVoxelShape),
321 Cube(CubeVoxelShape),
322}
323
324impl VoxelShape {
325 fn min(&self, axis: Axis) -> f64 {
334 let first_full = self.shape().first_full(axis);
335 if first_full >= self.shape().size(axis) as i32 {
336 f64::INFINITY
337 } else {
338 self.get(axis, first_full.try_into().unwrap())
339 }
340 }
341 fn max(&self, axis: Axis) -> f64 {
342 let last_full = self.shape().last_full(axis);
343 if last_full <= 0 {
344 f64::NEG_INFINITY
345 } else {
346 self.get(axis, last_full.try_into().unwrap())
347 }
348 }
349
350 pub fn shape(&self) -> &DiscreteVoxelShape {
351 match self {
352 VoxelShape::Array(s) => s.shape(),
353 VoxelShape::Cube(s) => s.shape(),
354 }
355 }
356
357 pub fn get_coords(&self, axis: Axis) -> &[f64] {
358 match self {
359 VoxelShape::Array(s) => s.get_coords(axis),
360 VoxelShape::Cube(s) => s.get_coords(axis),
361 }
362 }
363
364 pub fn is_empty(&self) -> bool {
365 self.shape().is_empty()
366 }
367
368 #[must_use]
369 pub fn move_relative(&self, delta: Vec3) -> VoxelShape {
370 if self.shape().is_empty() {
371 return EMPTY_SHAPE.clone();
372 }
373
374 VoxelShape::Array(ArrayVoxelShape::new(
375 self.shape().to_owned(),
376 self.get_coords(Axis::X)
377 .iter()
378 .map(|c| c + delta.x)
379 .collect(),
380 self.get_coords(Axis::Y)
381 .iter()
382 .map(|c| c + delta.y)
383 .collect(),
384 self.get_coords(Axis::Z)
385 .iter()
386 .map(|c| c + delta.z)
387 .collect(),
388 ))
389 }
390
391 #[inline]
392 pub fn get(&self, axis: Axis, index: usize) -> f64 {
393 match self {
395 VoxelShape::Array(s) => s.get_coords(axis)[index],
396 VoxelShape::Cube(s) => s.get_coords(axis)[index],
397 }
399 }
400
401 pub fn find_index(&self, axis: Axis, coord: f64) -> i32 {
402 match self {
403 VoxelShape::Cube(s) => s.find_index(axis, coord),
404 _ => {
405 let upper_limit = (self.shape().size(axis) + 1) as i32;
406 binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1
407 }
408 }
409 }
410
411 pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option<BlockHitResult> {
412 if self.is_empty() {
413 return None;
414 }
415 let vector = to - from;
416 if vector.length_squared() < EPSILON {
417 return None;
418 }
419 let right_after_start = from + &(vector * 0.001);
420
421 if self.shape().is_full_wide(
422 self.find_index(Axis::X, right_after_start.x - block_pos.x as f64),
423 self.find_index(Axis::Y, right_after_start.y - block_pos.y as f64),
424 self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64),
425 ) {
426 Some(BlockHitResult {
427 block_pos: *block_pos,
428 direction: Direction::nearest(vector).opposite(),
429 location: right_after_start,
430 inside: true,
431 miss: false,
432 world_border: false,
433 })
434 } else {
435 AABB::clip_iterable(&self.to_aabbs(), from, to, block_pos)
436 }
437 }
438
439 pub fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 {
440 self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement)
441 }
442 pub fn collide_x(&self, axis_cycle: AxisCycle, entity_box: &AABB, mut movement: f64) -> f64 {
443 if self.shape().is_empty() {
444 return movement;
445 }
446 if movement.abs() < EPSILON {
447 return 0.;
448 }
449
450 let inverse_axis_cycle = axis_cycle.inverse();
451
452 let x_axis = inverse_axis_cycle.cycle(Axis::X);
453 let y_axis = inverse_axis_cycle.cycle(Axis::Y);
454 let z_axis = inverse_axis_cycle.cycle(Axis::Z);
455
456 let max_x = entity_box.max(&x_axis);
457 let min_x = entity_box.min(&x_axis);
458
459 let x_min_index = self.find_index(x_axis, min_x + EPSILON);
460 let x_max_index = self.find_index(x_axis, max_x - EPSILON);
461
462 let y_min_index = cmp::max(
463 0,
464 self.find_index(y_axis, entity_box.min(&y_axis) + EPSILON),
465 );
466 let y_max_index = cmp::min(
467 self.shape().size(y_axis) as i32,
468 self.find_index(y_axis, entity_box.max(&y_axis) - EPSILON) + 1,
469 );
470
471 let z_min_index = cmp::max(
472 0,
473 self.find_index(z_axis, entity_box.min(&z_axis) + EPSILON),
474 );
475 let z_max_index = cmp::min(
476 self.shape().size(z_axis) as i32,
477 self.find_index(z_axis, entity_box.max(&z_axis) - EPSILON) + 1,
478 );
479
480 if movement > 0. {
481 for x in x_max_index + 1..(self.shape().size(x_axis) as i32) {
482 for y in y_min_index..y_max_index {
483 for z in z_min_index..z_max_index {
484 if self
485 .shape()
486 .is_full_wide_axis_cycle(inverse_axis_cycle, x, y, z)
487 {
488 let var23 = self.get(x_axis, x as usize) - max_x;
489 if var23 >= -EPSILON {
490 movement = f64::min(movement, var23);
491 }
492 return movement;
493 }
494 }
495 }
496 }
497 } else if movement < 0. && x_min_index > 0 {
498 for x in (0..x_min_index).rev() {
499 for y in y_min_index..y_max_index {
500 for z in z_min_index..z_max_index {
501 if self
502 .shape()
503 .is_full_wide_axis_cycle(inverse_axis_cycle, x, y, z)
504 {
505 let var23 = self.get(x_axis, (x + 1) as usize) - min_x;
506 if var23 <= EPSILON {
507 movement = f64::max(movement, var23);
508 }
509 return movement;
510 }
511 }
512 }
513 }
514 }
515
516 movement
517 }
518
519 fn optimize(&self) -> VoxelShape {
520 let mut shape = EMPTY_SHAPE.clone();
521 self.for_all_boxes(|var1x, var3, var5, var7, var9, var11| {
522 shape = Shapes::join_unoptimized(
523 shape.clone(),
524 box_shape(var1x, var3, var5, var7, var9, var11),
525 |a, b| a || b,
526 );
527 });
528 shape
529 }
530
531 pub fn for_all_boxes(&self, mut consumer: impl FnMut(f64, f64, f64, f64, f64, f64))
532 where
533 Self: Sized,
534 {
535 let x_coords = self.get_coords(Axis::X);
536 let y_coords = self.get_coords(Axis::Y);
537 let z_coords = self.get_coords(Axis::Z);
538 self.shape().for_all_boxes(
539 |min_x, min_y, min_z, max_x, max_y, max_z| {
540 consumer(
541 x_coords[min_x as usize],
542 y_coords[min_y as usize],
543 z_coords[min_z as usize],
544 x_coords[max_x as usize],
545 y_coords[max_y as usize],
546 z_coords[max_z as usize],
547 );
548 },
549 true,
550 );
551 }
552
553 pub fn to_aabbs(&self) -> Vec<AABB> {
554 let mut aabbs = Vec::new();
555 self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| {
556 aabbs.push(AABB {
557 min: Vec3::new(min_x, min_y, min_z),
558 max: Vec3::new(max_x, max_y, max_z),
559 });
560 });
561 aabbs
562 }
563
564 pub fn bounds(&self) -> AABB {
565 assert!(!self.is_empty(), "Can't get bounds for empty shape");
566 AABB {
567 min: Vec3::new(self.min(Axis::X), self.min(Axis::Y), self.min(Axis::Z)),
568 max: Vec3::new(self.max(Axis::X), self.max(Axis::Y), self.max(Axis::Z)),
569 }
570 }
571}
572
573impl From<&AABB> for VoxelShape {
574 fn from(aabb: &AABB) -> Self {
575 box_shape(
576 aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z,
577 )
578 }
579}
580impl From<AABB> for VoxelShape {
581 fn from(aabb: AABB) -> Self {
582 VoxelShape::from(&aabb)
583 }
584}
585
586#[derive(Clone, PartialEq, Debug)]
587pub struct ArrayVoxelShape {
588 shape: DiscreteVoxelShape,
589 #[allow(dead_code)]
591 faces: Option<Vec<VoxelShape>>,
592
593 pub xs: Vec<f64>,
594 pub ys: Vec<f64>,
595 pub zs: Vec<f64>,
596}
597
598#[derive(Clone, PartialEq, Debug)]
599pub struct CubeVoxelShape {
600 shape: DiscreteVoxelShape,
601 #[allow(dead_code)]
603 faces: Option<Vec<VoxelShape>>,
604
605 x_coords: Vec<f64>,
606 y_coords: Vec<f64>,
607 z_coords: Vec<f64>,
608}
609
610impl ArrayVoxelShape {
611 pub fn new(shape: DiscreteVoxelShape, xs: Vec<f64>, ys: Vec<f64>, zs: Vec<f64>) -> Self {
612 let x_size = shape.size(Axis::X) + 1;
613 let y_size = shape.size(Axis::Y) + 1;
614 let z_size = shape.size(Axis::Z) + 1;
615
616 debug_assert_eq!(x_size, xs.len() as u32);
618 debug_assert_eq!(y_size, ys.len() as u32);
619 debug_assert_eq!(z_size, zs.len() as u32);
620
621 Self {
622 faces: None,
623 shape,
624 xs,
625 ys,
626 zs,
627 }
628 }
629}
630
631impl ArrayVoxelShape {
632 fn shape(&self) -> &DiscreteVoxelShape {
633 &self.shape
634 }
635
636 #[inline]
637 fn get_coords(&self, axis: Axis) -> &[f64] {
638 axis.choose(&self.xs, &self.ys, &self.zs)
639 }
640}
641
642impl CubeVoxelShape {
643 pub fn new(shape: DiscreteVoxelShape) -> Self {
644 let x_coords = Self::calculate_coords(&shape, Axis::X);
645 let y_coords = Self::calculate_coords(&shape, Axis::Y);
646 let z_coords = Self::calculate_coords(&shape, Axis::Z);
647
648 Self {
649 shape,
650 faces: None,
651 x_coords,
652 y_coords,
653 z_coords,
654 }
655 }
656}
657
658impl CubeVoxelShape {
659 fn shape(&self) -> &DiscreteVoxelShape {
660 &self.shape
661 }
662
663 fn calculate_coords(shape: &DiscreteVoxelShape, axis: Axis) -> Vec<f64> {
664 let size = shape.size(axis);
665 let mut parts = Vec::with_capacity(size as usize);
666 for i in 0..=size {
667 parts.push(i as f64 / size as f64);
668 }
669 parts
670 }
671
672 #[inline]
673 fn get_coords(&self, axis: Axis) -> &[f64] {
674 axis.choose(&self.x_coords, &self.y_coords, &self.z_coords)
675 }
676
677 fn find_index(&self, axis: Axis, coord: f64) -> i32 {
678 let n = self.shape().size(axis);
679 f64::floor(f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32
680 }
681}
682
683#[derive(Debug)]
684pub struct CubePointRange {
685 pub parts: NonZeroU32,
687}
688impl CubePointRange {
689 pub fn get_double(&self, index: u32) -> f64 {
690 index as f64 / self.parts.get() as f64
691 }
692
693 pub fn size(&self) -> u32 {
694 self.parts.get() + 1
695 }
696
697 pub fn iter(&self) -> Vec<f64> {
698 (0..=self.parts.get()).map(|i| self.get_double(i)).collect()
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn test_block_shape() {
708 let shape = &*BLOCK_SHAPE;
709 assert_eq!(shape.shape().size(Axis::X), 1);
710 assert_eq!(shape.shape().size(Axis::Y), 1);
711 assert_eq!(shape.shape().size(Axis::Z), 1);
712
713 assert_eq!(shape.get_coords(Axis::X).len(), 2);
714 assert_eq!(shape.get_coords(Axis::Y).len(), 2);
715 assert_eq!(shape.get_coords(Axis::Z).len(), 2);
716 }
717
718 #[test]
719 fn test_box_shape() {
720 let shape = box_shape(0., 0., 0., 1., 1., 1.);
721 assert_eq!(shape.shape().size(Axis::X), 1);
722 assert_eq!(shape.shape().size(Axis::Y), 1);
723 assert_eq!(shape.shape().size(Axis::Z), 1);
724
725 assert_eq!(shape.get_coords(Axis::X).len(), 2);
726 assert_eq!(shape.get_coords(Axis::Y).len(), 2);
727 assert_eq!(shape.get_coords(Axis::Z).len(), 2);
728 }
729
730 #[test]
731 fn test_top_slab_shape() {
732 let shape = box_shape(0., 0.5, 0., 1., 1., 1.);
733 assert_eq!(shape.shape().size(Axis::X), 1);
734 assert_eq!(shape.shape().size(Axis::Y), 2);
735 assert_eq!(shape.shape().size(Axis::Z), 1);
736
737 assert_eq!(shape.get_coords(Axis::X).len(), 2);
738 assert_eq!(shape.get_coords(Axis::Y).len(), 3);
739 assert_eq!(shape.get_coords(Axis::Z).len(), 2);
740 }
741
742 #[test]
743 fn test_join_is_not_empty() {
744 let shape = box_shape(0., 0., 0., 1., 1., 1.);
745 let shape2 = box_shape(0., 0.5, 0., 1., 1., 1.);
746 let joined = Shapes::matches_anywhere(&shape, &shape2, |a, b| a && b);
748 assert!(joined, "Shapes should intersect");
749 }
750
751 #[test]
752 fn clip_in_front_of_block() {
753 let block_shape = &*BLOCK_SHAPE;
754 let block_hit_result = block_shape
755 .clip(
756 &Vec3::new(-0.3, 0.5, 0.),
757 &Vec3::new(5.3, 0.5, 0.),
758 &BlockPos::new(0, 0, 0),
759 )
760 .unwrap();
761
762 assert_eq!(
763 block_hit_result,
764 BlockHitResult {
765 location: Vec3 {
766 x: 0.0,
767 y: 0.5,
768 z: 0.0
769 },
770 direction: Direction::West,
771 block_pos: BlockPos { x: 0, y: 0, z: 0 },
772 inside: false,
773 world_border: false,
774 miss: false
775 }
776 );
777 }
778}