1// SuperTux
2// Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3// 2018 Ingo Ruhnke <grumbel@gmail.com>
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18#include "collision/collision_system.hpp"
19
20#include "collision/collision.hpp"
21#include "editor/editor.hpp"
22#include "math/aatriangle.hpp"
23#include "math/rect.hpp"
24#include "object/player.hpp"
25#include "object/tilemap.hpp"
26#include "supertux/constants.hpp"
27#include "supertux/sector.hpp"
28#include "supertux/tile.hpp"
29#include "video/color.hpp"
30#include "video/drawing_context.hpp"
31
32namespace {
33
34const float MAX_SPEED = 16.0f;
35
36// a small value... be careful as CD is very sensitive to it
37const float DELTA = .002f;
38
39} // namespace
40
41CollisionSystem::CollisionSystem(Sector& sector) :
42 m_sector(sector),
43 m_objects()
44{
45}
46
47void
48CollisionSystem::add(CollisionObject* object)
49{
50 m_objects.push_back(object);
51}
52
53void
54CollisionSystem::remove(CollisionObject* object)
55{
56 m_objects.erase(
57 std::find(m_objects.begin(), m_objects.end(),
58 object));
59}
60
61void
62CollisionSystem::draw(DrawingContext& context)
63{
64 const Color violet(0.5f, 0.0f, 1.0f, 0.75f);
65 const Color red(1.0f, 0.0f, 0.0f, 0.75f);
66 const Color red_bright(1.0f, 0.5f, 0.5f, 0.75f);
67 const Color cyan(0.0f, 1.0f, 1.0f, 0.75f);
68 const Color orange(1.0f, 0.5f, 0.0f, 0.75f);
69 const Color green_bright(0.7f, 1.0f, 0.7f, 0.75f);
70 for (auto& object : m_objects) {
71 Color color;
72 switch (object->get_group()) {
73 case COLGROUP_MOVING_STATIC:
74 color = violet;
75 break;
76 case COLGROUP_MOVING:
77 color = red;
78 break;
79 case COLGROUP_MOVING_ONLY_STATIC:
80 color = red_bright;
81 break;
82 case COLGROUP_STATIC:
83 color = cyan;
84 break;
85 case COLGROUP_TOUCHABLE:
86 color = orange;
87 break;
88 default:
89 color = green_bright;
90 }
91 const Rectf& rect = object->get_bbox();
92 context.color().draw_filled_rect(rect, color, LAYER_FOREGROUND1 + 10);
93 }
94}
95
96namespace {
97
98/** r1 is supposed to be moving, r2 a solid object */
99void check_collisions(collision::Constraints* constraints,
100 const Vector& obj_movement, const Rectf& obj_rect, const Rectf& other_rect,
101 CollisionObject* object = nullptr, CollisionObject* other = nullptr, const Vector& other_movement = Vector(0,0))
102{
103 if (!collision::intersects(obj_rect, other_rect))
104 return;
105
106 const CollisionHit dummy;
107 if (other != nullptr && object != nullptr && !other->collides(*object, dummy))
108 return;
109 if (object != nullptr && other != nullptr && !object->collides(*other, dummy))
110 return;
111
112 // calculate intersection
113 const float itop = obj_rect.get_bottom() - other_rect.get_top();
114 const float ibottom = other_rect.get_bottom() - obj_rect.get_top();
115 const float ileft = obj_rect.get_right() - other_rect.get_left();
116 const float iright = other_rect.get_right() - obj_rect.get_left();
117
118 if (fabsf(obj_movement.y) > fabsf(obj_movement.x)) {
119 if (ileft < SHIFT_DELTA) {
120 constraints->constrain_right(other_rect.get_left(), other_movement.x);
121 return;
122 } else if (iright < SHIFT_DELTA) {
123 constraints->constrain_left(other_rect.get_right(), other_movement.x);
124 return;
125 }
126 } else {
127 // shiftout bottom/top
128 if (itop < SHIFT_DELTA) {
129 constraints->constrain_bottom(other_rect.get_top(), other_movement.y);
130 return;
131 } else if (ibottom < SHIFT_DELTA) {
132 constraints->constrain_top(other_rect.get_bottom(), other_movement.y);
133 return;
134 }
135 }
136
137 constraints->ground_movement += other_movement;
138 if (other != nullptr && object != nullptr) {
139 const HitResponse response = other->collision(*object, dummy);
140 if (response == ABORT_MOVE)
141 return;
142
143 if (other->get_movement() != Vector(0, 0)) {
144 // TODO what todo when we collide with 2 moving objects?!?
145 constraints->ground_movement += other->get_movement();
146 }
147 }
148
149 const float vert_penetration = std::min(itop, ibottom);
150 const float horiz_penetration = std::min(ileft, iright);
151
152 if (vert_penetration < horiz_penetration) {
153 if (itop < ibottom) {
154 constraints->constrain_bottom(other_rect.get_top(), other_movement.y);
155 constraints->hit.bottom = true;
156 } else {
157 constraints->constrain_top(other_rect.get_bottom(), other_movement.y);
158 constraints->hit.top = true;
159 }
160 } else {
161 if (ileft < iright) {
162 constraints->constrain_right(other_rect.get_left(), other_movement.x);
163 constraints->hit.right = true;
164 } else {
165 constraints->constrain_left(other_rect.get_right(), other_movement.x);
166 constraints->hit.left = true;
167 }
168 }
169}
170
171} // namespace
172
173void
174CollisionSystem::collision_tilemap(collision::Constraints* constraints,
175 const Vector& movement, const Rectf& dest,
176 CollisionObject& object) const
177{
178 // calculate rectangle where the object will move
179 const float x1 = dest.get_left();
180 const float x2 = dest.get_right();
181 const float y1 = dest.get_top();
182 const float y2 = dest.get_bottom();
183
184 for (const auto& solids : m_sector.get_solid_tilemaps())
185 {
186 // test with all tiles in this rectangle
187 const Rect test_tiles = solids->get_tiles_overlapping(Rectf(x1, y1, x2, y2));
188
189 for (int x = test_tiles.left; x < test_tiles.right; ++x)
190 {
191 for (int y = test_tiles.top; y < test_tiles.bottom; ++y)
192 {
193 const Tile& tile = solids->get_tile(x, y);
194
195 // skip non-solid tiles
196 if (!tile.is_solid ())
197 continue;
198 Rectf tile_bbox = solids->get_tile_bbox(x, y);
199
200 /* If the tile is a unisolid tile, the "is_solid()" function above
201 * didn't do a thorough check. Calculate the position and (relative)
202 * movement of the object and determine whether or not the tile is
203 * solid with regard to those parameters. */
204 if (tile.is_unisolid ()) {
205 Vector relative_movement = movement
206 - solids->get_movement(/* actual = */ true);
207
208 if (!tile.is_solid (tile_bbox, object.get_bbox(), relative_movement))
209 continue;
210 }
211
212 if (tile.is_slope ()) { // slope tile
213 AATriangle triangle;
214 int slope_data = tile.get_data();
215 if (solids->get_flip() & VERTICAL_FLIP)
216 slope_data = AATriangle::vertical_flip(slope_data);
217 triangle = AATriangle(tile_bbox, slope_data);
218
219 collision::rectangle_aatriangle(constraints, dest, triangle,
220 solids->get_movement(/* actual = */ false));
221 } else { // normal rectangular tile
222 check_collisions(constraints, movement, dest, tile_bbox, nullptr, nullptr,
223 solids->get_movement(/* actual = */ false));
224 }
225 }
226 }
227 }
228}
229
230uint32_t
231CollisionSystem::collision_tile_attributes(const Rectf& dest, const Vector& mov) const
232{
233 const float x1 = dest.get_left();
234 const float y1 = dest.get_top();
235 const float x2 = dest.get_right();
236 const float y2 = dest.get_bottom();
237
238 uint32_t result = 0;
239 for (auto& solids: m_sector.get_solid_tilemaps())
240 {
241 // test with all tiles in this rectangle
242 const Rect test_tiles = solids->get_tiles_overlapping(Rectf(x1, y1, x2, y2));
243
244 // For ice (only), add a little fudge to recognize tiles Tux is standing on.
245 const Rect test_tiles_ice = solids->get_tiles_overlapping(Rectf(x1, y1, x2, y2 + SHIFT_DELTA));
246
247 for (int x = test_tiles.left; x < test_tiles.right; ++x) {
248 int y;
249 for (y = test_tiles.top; y < test_tiles.bottom; ++y) {
250 const Tile& tile = solids->get_tile(x, y);
251
252 if ( tile.is_collisionful( solids->get_tile_bbox(x, y), dest, mov) ) {
253 result |= tile.get_attributes();
254 }
255 }
256 for (; y < test_tiles_ice.bottom; ++y) {
257 const Tile& tile = solids->get_tile(x, y);
258 if ( tile.is_collisionful( solids->get_tile_bbox(x, y), dest, mov) ) {
259 result |= (tile.get_attributes() & Tile::ICE);
260 }
261 }
262 }
263 }
264
265 return result;
266}
267
268/** fills in CollisionHit and Normal vector of 2 intersecting rectangle */
269static void get_hit_normal(const Rectf& r1, const Rectf& r2, CollisionHit& hit,
270 Vector& normal)
271{
272 const float itop = r1.get_bottom() - r2.get_top();
273 const float ibottom = r2.get_bottom() - r1.get_top();
274 const float ileft = r1.get_right() - r2.get_left();
275 const float iright = r2.get_right() - r1.get_left();
276
277 const float vert_penetration = std::min(itop, ibottom);
278 const float horiz_penetration = std::min(ileft, iright);
279
280 if (vert_penetration < horiz_penetration) {
281 if (itop < ibottom) {
282 hit.bottom = true;
283 normal.y = vert_penetration;
284 } else {
285 hit.top = true;
286 normal.y = -vert_penetration;
287 }
288 } else {
289 if (ileft < iright) {
290 hit.right = true;
291 normal.x = horiz_penetration;
292 } else {
293 hit.left = true;
294 normal.x = -horiz_penetration;
295 }
296 }
297}
298
299void
300CollisionSystem::collision_object(CollisionObject* object1, CollisionObject* object2) const
301{
302 using namespace collision;
303
304 const Rectf& r1 = object1->m_dest;
305 const Rectf& r2 = object2->m_dest;
306
307 CollisionHit hit;
308 if (intersects(object1->m_dest, object2->m_dest)) {
309 Vector normal;
310 get_hit_normal(r1, r2, hit, normal);
311
312 if (!object1->collides(*object2, hit))
313 return;
314 std::swap(hit.left, hit.right);
315 std::swap(hit.top, hit.bottom);
316 if (!object2->collides(*object1, hit))
317 return;
318 std::swap(hit.left, hit.right);
319 std::swap(hit.top, hit.bottom);
320
321 HitResponse response1 = object1->collision(*object2, hit);
322 std::swap(hit.left, hit.right);
323 std::swap(hit.top, hit.bottom);
324 HitResponse response2 = object2->collision(*object1, hit);
325 if (response1 == CONTINUE && response2 == CONTINUE) {
326 normal *= (0.5f + DELTA);
327 object1->m_dest.move(-normal);
328 object2->m_dest.move(normal);
329 } else if (response1 == CONTINUE && response2 == FORCE_MOVE) {
330 normal *= (1 + DELTA);
331 object1->m_dest.move(-normal);
332 } else if (response1 == FORCE_MOVE && response2 == CONTINUE) {
333 normal *= (1 + DELTA);
334 object2->m_dest.move(normal);
335 }
336 }
337}
338
339void
340CollisionSystem::collision_static(collision::Constraints* constraints,
341 const Vector& movement, const Rectf& dest,
342 CollisionObject& object)
343{
344 collision_tilemap(constraints, movement, dest, object);
345
346 // collision with other (static) objects
347 for (auto& static_object : m_objects)
348 {
349 if (static_object->get_group() != COLGROUP_STATIC &&
350 static_object->get_group() != COLGROUP_MOVING_STATIC)
351 continue;
352 if (!static_object->is_valid())
353 continue;
354
355 if (static_object != &object) {
356 check_collisions(constraints, movement, dest, static_object->m_bbox,
357 &object, static_object);
358 }
359 }
360}
361
362void
363CollisionSystem::collision_static_constrains(CollisionObject& object)
364{
365 using namespace collision;
366
367 const float infinity = (std::numeric_limits<float>::has_infinity ? std::numeric_limits<float>::infinity() : std::numeric_limits<float>::max());
368
369 Constraints constraints;
370 const Vector movement = object.get_movement();
371 Vector pressure = Vector(0,0);
372 Rectf& dest = object.m_dest;
373
374 for (int i = 0; i < 2; ++i) {
375 collision_static(&constraints, Vector(0, movement.y), dest, object);
376 if (!constraints.has_constraints())
377 break;
378
379 // apply calculated horizontal constraints
380 if (constraints.get_position_bottom() < infinity) {
381 float height = constraints.get_height ();
382 if (height < object.get_bbox().get_height()) {
383 // we're crushed, but ignore this for now, we'll get this again
384 // later if we're really crushed or things will solve itself when
385 // looking at the vertical constraints
386 pressure.y += object.get_bbox().get_height() - height;
387 } else {
388 dest.set_bottom(constraints.get_position_bottom() - DELTA);
389 dest.set_top(dest.get_bottom() - object.get_bbox().get_height());
390 }
391 } else if (constraints.get_position_top() > -infinity) {
392 dest.set_top(constraints.get_position_top() + DELTA);
393 dest.set_bottom(dest.get_top() + object.get_bbox().get_height());
394 }
395 }
396
397 if (constraints.has_constraints()) {
398 if (constraints.hit.bottom) {
399 dest.move(constraints.ground_movement);
400 }
401
402 if (constraints.hit.top || constraints.hit.bottom) {
403 constraints.hit.left = false;
404 constraints.hit.right = false;
405 object.collision_solid(constraints.hit);
406 }
407 }
408
409 constraints = Constraints();
410 for (int i = 0; i < 2; ++i) {
411 collision_static(&constraints, movement, dest, object);
412 if (!constraints.has_constraints())
413 break;
414
415 // apply calculated vertical constraints
416 const float width = constraints.get_width();
417
418 if (width < infinity) {
419 if (width + SHIFT_DELTA < object.get_bbox().get_width()) {
420 // we're crushed, but ignore this for now, we'll get this again
421 // later if we're really crushed or things will solve itself when
422 // looking at the horizontal constraints
423 pressure.x += object.get_bbox().get_width() - width;
424 } else {
425 float xmid = constraints.get_x_midpoint ();
426 dest.set_left(xmid - object.get_bbox().get_width()/2);
427 dest.set_right(xmid + object.get_bbox().get_width()/2);
428 }
429 } else if (constraints.get_position_right() < infinity) {
430 dest.set_right(constraints.get_position_right() - DELTA);
431 dest.set_left(dest.get_right() - object.get_bbox().get_width());
432 } else if (constraints.get_position_left() > -infinity) {
433 dest.set_left(constraints.get_position_left() + DELTA);
434 dest.set_right(dest.get_left() + object.get_bbox().get_width());
435 }
436 }
437
438 if (constraints.has_constraints()) {
439 if ( constraints.hit.left || constraints.hit.right
440 || constraints.hit.top || constraints.hit.bottom
441 || constraints.hit.crush )
442 object.collision_solid(constraints.hit);
443 }
444
445 // an extra pass to make sure we're not crushed vertically
446 if (pressure.y > 0) {
447 constraints = Constraints();
448 collision_static(&constraints, movement, dest, object);
449 if (constraints.get_position_bottom() < infinity) {
450 const float height = constraints.get_height ();
451
452 if (height + SHIFT_DELTA < object.get_bbox().get_height()) {
453 CollisionHit h;
454 h.top = true;
455 h.bottom = true;
456 h.crush = pressure.y > 16;
457 object.collision_solid(h);
458 }
459 }
460 }
461
462 // an extra pass to make sure we're not crushed horizontally
463 if (pressure.x > 0) {
464 constraints = Constraints();
465 collision_static(&constraints, movement, dest, object);
466 if (constraints.get_position_right() < infinity) {
467 float width = constraints.get_width ();
468 if (width + SHIFT_DELTA < object.get_bbox().get_width()) {
469 CollisionHit h;
470 h.top = true;
471 h.bottom = true;
472 h.left = true;
473 h.right = true;
474 h.crush = pressure.x > 16;
475 object.collision_solid(h);
476 }
477 }
478 }
479}
480
481void
482CollisionSystem::update()
483{
484 if (Editor::is_active()) {
485 return;
486 //Oběcts in editor shouldn't collide.
487 }
488
489 using namespace collision;
490
491 // calculate destination positions of the objects
492 for (const auto& object : m_objects)
493 {
494 const Vector mov = object->get_movement();
495
496 // make sure movement is never faster than MAX_SPEED. Norm is pretty fat, so two addl. checks are done before.
497 if (((mov.x > MAX_SPEED * static_cast<float>(M_SQRT1_2)) || (mov.y > MAX_SPEED * static_cast<float>(M_SQRT1_2))) && (mov.norm() > MAX_SPEED)) {
498 object->m_movement = mov.unit() * MAX_SPEED;
499 //log_debug << "Temporarily reduced object's speed of " << mov.norm() << " to " << object->movement.norm() << "." << std::endl;
500 }
501
502 object->m_dest = object->get_bbox();
503 object->m_dest.move(object->get_movement());
504 }
505
506 // part1: COLGROUP_MOVING vs COLGROUP_STATIC and tilemap
507 for (const auto& object : m_objects) {
508 if ((object->get_group() != COLGROUP_MOVING
509 && object->get_group() != COLGROUP_MOVING_STATIC
510 && object->get_group() != COLGROUP_MOVING_ONLY_STATIC)
511 || !object->is_valid())
512 continue;
513
514 collision_static_constrains(*object);
515 }
516
517 // part2: COLGROUP_MOVING vs tile attributes
518 for (const auto& object : m_objects) {
519 if ((object->get_group() != COLGROUP_MOVING
520 && object->get_group() != COLGROUP_MOVING_STATIC
521 && object->get_group() != COLGROUP_MOVING_ONLY_STATIC)
522 || !object->is_valid())
523 continue;
524
525 uint32_t tile_attributes = collision_tile_attributes(object->m_dest, object->get_movement());
526 if (tile_attributes >= Tile::FIRST_INTERESTING_FLAG) {
527 object->collision_tile(tile_attributes);
528 }
529 }
530
531 // part2.5: COLGROUP_MOVING vs COLGROUP_TOUCHABLE
532 for (const auto& object : m_objects)
533 {
534 if ((object->get_group() != COLGROUP_MOVING
535 && object->get_group() != COLGROUP_MOVING_STATIC)
536 || !object->is_valid())
537 continue;
538
539 for (auto& object_2 : m_objects) {
540 if (object_2->get_group() != COLGROUP_TOUCHABLE
541 || !object_2->is_valid())
542 continue;
543
544 if (intersects(object->m_dest, object_2->m_dest)) {
545 Vector normal;
546 CollisionHit hit;
547 get_hit_normal(object->m_dest, object_2->m_dest,
548 hit, normal);
549 if (!object->collides(*object_2, hit))
550 continue;
551 if (!object_2->collides(*object, hit))
552 continue;
553
554 object->collision(*object_2, hit);
555 object_2->collision(*object, hit);
556 }
557 }
558 }
559
560 // part3: COLGROUP_MOVING vs COLGROUP_MOVING
561 for (auto i = m_objects.begin(); i != m_objects.end(); ++i)
562 {
563 auto object = *i;
564
565 if ((object->get_group() != COLGROUP_MOVING
566 && object->get_group() != COLGROUP_MOVING_STATIC)
567 || !object->is_valid())
568 continue;
569
570 for (auto i2 = i+1; i2 != m_objects.end(); ++i2) {
571 auto object_2 = *i2;
572 if ((object_2->get_group() != COLGROUP_MOVING
573 && object_2->get_group() != COLGROUP_MOVING_STATIC)
574 || !object_2->is_valid())
575 continue;
576
577 collision_object(object, object_2);
578 }
579 }
580
581 // apply object movement
582 for (const auto& object : m_objects) {
583 object->m_bbox = object->m_dest;
584 object->m_movement = Vector(0, 0);
585 }
586}
587
588bool
589CollisionSystem::is_free_of_tiles(const Rectf& rect, const bool ignoreUnisolid) const
590{
591 using namespace collision;
592
593 for (const auto& solids : m_sector.get_solid_tilemaps()) {
594 // test with all tiles in this rectangle
595 const Rect test_tiles = solids->get_tiles_overlapping(rect);
596
597 for (int x = test_tiles.left; x < test_tiles.right; ++x) {
598 for (int y = test_tiles.top; y < test_tiles.bottom; ++y) {
599 const Tile& tile = solids->get_tile(x, y);
600
601 if (!(tile.get_attributes() & Tile::SOLID))
602 continue;
603 if (tile.is_unisolid () && ignoreUnisolid)
604 continue;
605 if (tile.is_slope ()) {
606 AATriangle triangle;
607 const Rectf tbbox = solids->get_tile_bbox(x, y);
608 triangle = AATriangle(tbbox, tile.get_data());
609 Constraints constraints;
610 if (!collision::rectangle_aatriangle(&constraints, rect, triangle))
611 continue;
612 }
613 // We have a solid tile that overlaps the given rectangle.
614 return false;
615 }
616 }
617 }
618
619 return true;
620}
621
622bool
623CollisionSystem::is_free_of_statics(const Rectf& rect, const CollisionObject* ignore_object, const bool ignoreUnisolid) const
624{
625 using namespace collision;
626
627 if (!is_free_of_tiles(rect, ignoreUnisolid)) return false;
628
629 for (const auto& object : m_objects) {
630 if (object == ignore_object) continue;
631 if (!object->is_valid()) continue;
632 if (object->get_group() == COLGROUP_STATIC) {
633 if (intersects(rect, object->get_bbox())) return false;
634 }
635 }
636
637 return true;
638}
639
640bool
641CollisionSystem::is_free_of_movingstatics(const Rectf& rect, const CollisionObject* ignore_object) const
642{
643 using namespace collision;
644
645 if (!is_free_of_tiles(rect)) return false;
646
647 for (const auto& object : m_objects) {
648 if (object == ignore_object) continue;
649 if (!object->is_valid()) continue;
650 if ((object->get_group() == COLGROUP_MOVING)
651 || (object->get_group() == COLGROUP_MOVING_STATIC)
652 || (object->get_group() == COLGROUP_STATIC)) {
653 if (intersects(rect, object->get_bbox())) return false;
654 }
655 }
656
657 return true;
658}
659
660bool
661CollisionSystem::free_line_of_sight(const Vector& line_start, const Vector& line_end, const CollisionObject* ignore_object) const
662{
663 using namespace collision;
664
665 // check if no tile is in the way
666 const float lsx = std::min(line_start.x, line_end.x);
667 const float lex = std::max(line_start.x, line_end.x);
668 const float lsy = std::min(line_start.y, line_end.y);
669 const float ley = std::max(line_start.y, line_end.y);
670
671 for (float test_x = lsx; test_x <= lex; test_x += 16) { // NOLINT
672 for (float test_y = lsy; test_y <= ley; test_y += 16) { // NOLINT
673 for (const auto& solids : m_sector.get_solid_tilemaps()) {
674 const Tile& tile = solids->get_tile_at(Vector(test_x, test_y));
675 // FIXME: check collision with slope tiles
676 if ((tile.get_attributes() & Tile::SOLID)) return false;
677 }
678 }
679 }
680
681 // check if no object is in the way
682 for (const auto& object : m_objects) {
683 if (object == ignore_object) continue;
684 if (!object->is_valid()) continue;
685 if ((object->get_group() == COLGROUP_MOVING)
686 || (object->get_group() == COLGROUP_MOVING_STATIC)
687 || (object->get_group() == COLGROUP_STATIC)) {
688 if (intersects_line(object->get_bbox(), line_start, line_end)) return false;
689 }
690 }
691
692 return true;
693}
694
695std::vector<CollisionObject*>
696CollisionSystem::get_nearby_objects (const Vector& center, float max_distance) const
697{
698 std::vector<CollisionObject*> ret;
699
700 for (const auto& object : m_objects) {
701 float distance = object->get_bbox().distance(center);
702 if (distance <= max_distance)
703 ret.push_back(object);
704 }
705
706 return ret;
707}
708
709/* EOF */
710