Line data Source code
1 0 : /*
2 :
3 : CollisionRegion.m
4 :
5 : Oolite
6 : Copyright (C) 2004-2013 Giles C Williams and contributors
7 :
8 : This program is free software; you can redistribute it and/or
9 : modify it under the terms of the GNU General Public License
10 : as published by the Free Software Foundation; either version 2
11 : of the License, or (at your option) any later version.
12 :
13 : This program is distributed in the hope that it will be useful,
14 : but WITHOUT ANY WARRANTY; without even the implied warranty of
15 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 : GNU General Public License for more details.
17 :
18 : You should have received a copy of the GNU General Public License
19 : along with this program; if not, write to the Free Software
20 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 : MA 02110-1301, USA.
22 :
23 : */
24 :
25 : #import "CollisionRegion.h"
26 : #import "OOMaths.h"
27 : #import "Universe.h"
28 : #import "Entity.h"
29 : #import "ShipEntity.h"
30 : #import "OOSunEntity.h"
31 : #import "OOPlanetEntity.h"
32 : #import "StationEntity.h"
33 : #import "PlayerEntity.h"
34 : #import "OODebugFlags.h"
35 :
36 :
37 0 : static BOOL positionIsWithinRegion(HPVector position, CollisionRegion *region);
38 0 : static BOOL sphereIsWithinRegion(HPVector position, GLfloat rad, CollisionRegion *region);
39 0 : static BOOL positionIsWithinBorders(HPVector position, CollisionRegion *region);
40 :
41 :
42 : @implementation CollisionRegion
43 :
44 : // basic alloc/ dealloc routines
45 : //
46 0 : static int crid_counter = 1;
47 :
48 :
49 0 : - (id) init // Designated initializer.
50 : {
51 : if ((self = [super init]))
52 : {
53 : max_entities = COLLISION_MAX_ENTITIES;
54 : entity_array = (Entity **)malloc(max_entities * sizeof(Entity *));
55 : if (entity_array == NULL)
56 : {
57 : [self release];
58 : return nil;
59 : }
60 :
61 : crid = crid_counter++;
62 : }
63 : return self;
64 : }
65 :
66 :
67 : - (id) initAsUniverse
68 : {
69 : if ((self = [self init]))
70 : {
71 : isUniverse = YES;
72 : }
73 : return self;
74 : }
75 :
76 :
77 : - (id) initAtLocation:(HPVector)locn withRadius:(GLfloat)rad withinRegion:(CollisionRegion *)otherRegion
78 : {
79 : if ((self = [self init]))
80 : {
81 : location = locn;
82 : radius = rad;
83 : border_radius = COLLISION_REGION_BORDER_RADIUS;
84 : parentRegion = otherRegion;
85 : }
86 : return self;
87 : }
88 :
89 :
90 0 : - (void) dealloc
91 : {
92 : free(entity_array);
93 : DESTROY(subregions);
94 :
95 : [super dealloc];
96 : }
97 :
98 :
99 0 : - (NSString *) description
100 : {
101 : return [NSString stringWithFormat:@"<%@ %p>{ID: %d, %lu subregions, %u ents}", [self class], self, crid, [subregions count], n_entities];
102 : }
103 :
104 :
105 : - (void) clearSubregions
106 : {
107 : [subregions makeObjectsPerformSelector:@selector(clearSubregions)];
108 : [subregions removeAllObjects];
109 : }
110 :
111 :
112 : - (void) addSubregionAtPosition:(HPVector)pos withRadius:(GLfloat)rad
113 : {
114 : // check if this can be fitted within any of the subregions
115 : //
116 : CollisionRegion *sub = nil;
117 : foreach (sub, subregions)
118 : {
119 : if (sphereIsWithinRegion(pos, rad, sub))
120 : {
121 : // if it fits, put it in!
122 : [sub addSubregionAtPosition:pos withRadius:rad];
123 : return;
124 : }
125 : if (positionIsWithinRegion(pos, sub))
126 : {
127 : // crosses the border of this region already - leave it out
128 : return;
129 : }
130 : }
131 : // no subregion fit - move on...
132 : //
133 : sub = [[CollisionRegion alloc] initAtLocation:pos withRadius:rad withinRegion:self];
134 : if (subregions == nil) subregions = [[NSMutableArray alloc] initWithCapacity:32];
135 : [subregions addObject:sub];
136 : [sub release];
137 : }
138 :
139 :
140 : // update routines to check if a position is within the radius or within its borders
141 : //
142 0 : static BOOL positionIsWithinRegion(HPVector position, CollisionRegion *region)
143 : {
144 : if (region == nil) return NO;
145 : if (region->isUniverse) return YES;
146 :
147 : HPVector loc = region->location;
148 : GLfloat r1 = region->radius;
149 :
150 : if ((position.x < loc.x - r1)||(position.x > loc.x + r1)||
151 : (position.y < loc.y - r1)||(position.y > loc.y + r1)||
152 : (position.z < loc.z - r1)||(position.z > loc.z + r1))
153 : {
154 : return NO;
155 : }
156 :
157 : return YES;
158 : }
159 :
160 :
161 0 : static BOOL sphereIsWithinRegion(HPVector position, GLfloat rad, CollisionRegion *region)
162 : {
163 : if (region == nil) return NO;
164 : if (region->isUniverse) return YES;
165 :
166 : HPVector loc = region->location;
167 : GLfloat r1 = region->radius;
168 :
169 : if ((position.x - rad < loc.x - r1)||(position.x + rad > loc.x + r1)||
170 : (position.y - rad < loc.y - r1)||(position.y + rad > loc.y + r1)||
171 : (position.z - rad < loc.z - r1)||(position.z + rad > loc.z + r1))
172 : {
173 : return NO;
174 : }
175 :
176 : return YES;
177 : }
178 :
179 :
180 0 : static BOOL positionIsWithinBorders(HPVector position, CollisionRegion *region)
181 : {
182 : if (region == nil) return NO;
183 : if (region->isUniverse) return YES;
184 :
185 : HPVector loc = region->location;
186 : GLfloat r1 = region->radius + region->border_radius;
187 :
188 : if ((position.x < loc.x - r1)||(position.x > loc.x + r1)||
189 : (position.y < loc.y - r1)||(position.y > loc.y + r1)||
190 : (position.z < loc.z - r1)||(position.z > loc.z + r1))
191 : {
192 : return NO;
193 : }
194 :
195 : return YES;
196 : }
197 :
198 :
199 : // collision checking
200 : //
201 : - (void) clearEntityList
202 : {
203 : [subregions makeObjectsPerformSelector:@selector(clearEntityList)];
204 : n_entities = 0;
205 : isPlayerInRegion = NO;
206 : }
207 :
208 :
209 : - (void) addEntity:(Entity *)ent
210 : {
211 : // expand if necessary
212 : //
213 : if (n_entities == max_entities)
214 : {
215 : max_entities = 1 + max_entities * 2;
216 : Entity **new_store = (Entity **)realloc(entity_array, max_entities * sizeof(Entity *));
217 : if (new_store == NULL)
218 : {
219 : [NSException raise:NSMallocException format:@"Not enough memory to grow collision region member list."];
220 : }
221 :
222 : entity_array = new_store;
223 : }
224 :
225 : if ([ent isPlayer]) isPlayerInRegion = YES;
226 : entity_array[n_entities++] = ent;
227 : }
228 :
229 :
230 : - (BOOL) checkEntity:(Entity *)ent
231 : {
232 : HPVector position = ent->position;
233 :
234 : // check subregions
235 : CollisionRegion *sub = nil;
236 : foreach (sub, subregions)
237 : {
238 : if (positionIsWithinBorders(position, sub) && [sub checkEntity:ent])
239 : {
240 : return YES;
241 : }
242 : }
243 :
244 : if (!positionIsWithinBorders(position, self))
245 : {
246 : return NO;
247 : }
248 :
249 : [self addEntity:ent];
250 : [ent setCollisionRegion:self];
251 : return YES;
252 : }
253 :
254 :
255 : - (void) findCollisions
256 : {
257 : // test for collisions in each subregion
258 : [subregions makeObjectsPerformSelector:@selector(findCollisions)];
259 :
260 : // reject trivial cases
261 : if (n_entities < 2) return;
262 :
263 : //
264 : // According to Shark, when this was in Universe this was where Oolite spent most time!
265 : //
266 : Entity *e1, *e2;
267 : HPVector p1;
268 : double dist2, r1, r2, r0, min_dist2;
269 : unsigned i;
270 : Entity *entities_to_test[n_entities];
271 :
272 : // only check unfiltered entities
273 : unsigned n_entities_to_test = 0;
274 : for (i = 0; i < n_entities; i++)
275 : {
276 : e1 = entity_array[i];
277 : if (e1->collisionTestFilter != 3)
278 : {
279 : entities_to_test[n_entities_to_test++] = e1;
280 : }
281 : }
282 :
283 : #ifndef NDEBUG
284 : if (gDebugFlags & DEBUG_COLLISIONS)
285 : {
286 : OOLog(@"collisionRegion.debug", @"DEBUG in collision region %@ testing %d out of %d entities", self, n_entities_to_test, n_entities);
287 : }
288 : #endif
289 :
290 : if (n_entities_to_test < 2) return;
291 :
292 : // clear collision variables
293 : //
294 : for (i = 0; i < n_entities_to_test; i++)
295 : {
296 : e1 = entities_to_test[i];
297 : if (e1->hasCollided)
298 : {
299 : [[e1 collisionArray] removeAllObjects];
300 : e1->hasCollided = NO;
301 : }
302 : if (e1->isShip)
303 : {
304 : [(ShipEntity*)e1 setProximityAlert:nil];
305 : }
306 : e1->collider = nil;
307 : }
308 :
309 : checks_this_tick = 0;
310 : checks_within_range = 0;
311 :
312 : // test each entity in this region against the entities in its collision chain
313 : //
314 : for (i = 0; i < n_entities_to_test; i++)
315 : {
316 : e1 = entities_to_test[i];
317 : p1 = e1->position;
318 : r1 = e1->collision_radius;
319 :
320 :
321 :
322 : // check against the first in the collision chain
323 : e2 = e1->collision_chain;
324 : while (e2 != nil)
325 : {
326 : checks_this_tick++;
327 : if (e1->isShip && e2->isShip &&
328 : [(ShipEntity *)e1 collisionExceptedFor:(ShipEntity *)e2])
329 : {
330 : // nothing happens
331 : }
332 : else
333 : {
334 :
335 : r2 = e2->collision_radius;
336 : r0 = r1 + r2;
337 : dist2 = HPdistance2(e2->position, p1);
338 : min_dist2 = r0 * r0;
339 : if (dist2 < PROXIMITY_WARN_DISTANCE2 * min_dist2)
340 : {
341 : #ifndef NDEBUG
342 : if (gDebugFlags & DEBUG_COLLISIONS)
343 : {
344 : OOLog(@"collisionRegion.debug", @"DEBUG Testing collision between %@ (%@) and %@ (%@)",
345 : e1, (e1->collisionTestFilter==3)?@"YES":@"NO", e2, (e2->collisionTestFilter==3)?@"YES":@"NO");
346 : }
347 : #endif
348 : checks_within_range++;
349 :
350 : if (e1->isShip && e2->isShip)
351 : {
352 : if ((dist2 < PROXIMITY_WARN_DISTANCE2 * r2 * r2) || (dist2 < PROXIMITY_WARN_DISTANCE2 * r1 * r1))
353 : {
354 : [(ShipEntity*)e1 setProximityAlert:(ShipEntity*)e2];
355 : [(ShipEntity*)e2 setProximityAlert:(ShipEntity*)e1];
356 : }
357 :
358 : if (dist2 >= min_dist2)
359 : {
360 : if (e1->isStation)
361 : {
362 : StationEntity* se1 = (StationEntity *)e1;
363 : [se1 shipIsInDockingCorridor:(ShipEntity *)e2];
364 : }
365 : else if (e2->isStation)
366 : {
367 : StationEntity* se2 = (StationEntity *)e2;
368 : [se2 shipIsInDockingCorridor:(ShipEntity *)e1];
369 : }
370 : }
371 :
372 : }
373 : if (dist2 < min_dist2)
374 : {
375 : BOOL collision = NO;
376 :
377 : if (e1->isStation)
378 : {
379 : StationEntity* se1 = (StationEntity *)e1;
380 : if ([se1 shipIsInDockingCorridor:(ShipEntity *)e2])
381 : {
382 : collision = NO;
383 : }
384 : else
385 : {
386 : collision = [e1 checkCloseCollisionWith:e2];
387 : }
388 : }
389 : else if (e2->isStation)
390 : {
391 : StationEntity* se2 = (StationEntity *)e2;
392 : if ([se2 shipIsInDockingCorridor:(ShipEntity *)e1])
393 : {
394 : collision = NO;
395 : }
396 : else
397 : {
398 : collision = [e2 checkCloseCollisionWith:e1];
399 : }
400 : }
401 : else
402 : {
403 : collision = [e1 checkCloseCollisionWith:e2];
404 : }
405 :
406 : if (collision)
407 : {
408 : // now we have no need to check the e2-e1 collision
409 : if (e1->collider)
410 : {
411 : [[e1 collisionArray] addObject:e1->collider];
412 : }
413 : else
414 : {
415 : [[e1 collisionArray] addObject:e2];
416 : }
417 : e1->hasCollided = YES;
418 :
419 : if (e2->collider)
420 : {
421 : [[e2 collisionArray] addObject:e2->collider];
422 : }
423 : else
424 : {
425 : [[e2 collisionArray] addObject:e1];
426 : }
427 : e2->hasCollided = YES;
428 : }
429 : }
430 : }
431 : }
432 : // check the next in the collision chain
433 : e2 = e2->collision_chain;
434 : }
435 : }
436 :
437 : #ifndef NDEBUG
438 : if (gDebugFlags & DEBUG_COLLISIONS)
439 : {
440 : OOLog(@"collisionRegion.debug",@"Collision test checks %d, within range %d, for %d entities",checks_this_tick,checks_within_range,n_entities_to_test);
441 : }
442 : #endif
443 : }
444 :
445 :
446 : // an outValue of 1 means it's just being occluded.
447 0 : static BOOL entityByEntityOcclusionToValue(Entity *e1, Entity *e2, OOSunEntity *the_sun, float *outValue)
448 : {
449 : if (EXPECT_NOT(e1 == e2))
450 : {
451 : // you can't shade self
452 : return NO;
453 : }
454 : return shadowAtPointOcclusionToValue(e1->position,e1->collision_radius,e2,the_sun,outValue);
455 : }
456 :
457 : // an outValue of 1 means it's just being occluded.
458 0 : BOOL shadowAtPointOcclusionToValue(HPVector e1pos, GLfloat e1rad, Entity *e2, OOSunEntity *the_sun, float *outValue)
459 : {
460 : *outValue = 1.5f; // initial 'fully lit' value
461 :
462 : GLfloat cr_e2;
463 : if ([e2 isShip])
464 : {
465 : cr_e2 = e2->collision_radius * 0.90f;
466 : // 10% smaller shadow for ships
467 : }
468 : else
469 : {
470 : cr_e2 = e2->collision_radius;
471 : }
472 : if (cr_e2 < e1rad)
473 : {
474 : // smaller can't shade bigger
475 : return NO;
476 : }
477 :
478 : // tested in construction of e2 list
479 : // if (e2->isSunlit == NO)
480 : // return NO; // things already /in/ shade can't shade things more.
481 : //
482 : // check projected sizes of discs
483 : GLfloat d2_sun = HPdistance2(e1pos, the_sun->position);
484 : GLfloat d2_e2sun = HPdistance2(e2->position, the_sun->position);
485 : GLfloat d2_e2 = HPdistance2( e1pos, e2->position);
486 :
487 : if (d2_e2sun > d2_sun)
488 : {
489 : // you are nearer the sun than the potential occluder, so it
490 : // probably can't shade you
491 : if (d2_e2 < cr_e2 * cr_e2 && [e2 isShip])
492 : {
493 : // exception: if within the collision radius of the other
494 : // object, might still be shadowed by it.
495 : GLfloat bbx = 0.0f, bby = 0.0f, bbz = 0.0f;
496 : BoundingBox bb = [(ShipEntity*)e2 totalBoundingBox];
497 : bounding_box_get_dimensions(bb,&bbx,&bby,&bbz);
498 : float minbb = bbx;
499 : if (bby < minbb) { minbb = bby; }
500 : if (bbz < minbb) { minbb = bbz; }
501 : minbb -= e1rad; // subtract object's size
502 : /* closer to the object than the shortest axis. This check
503 : * branch is basically for docking at a rock hermit facing
504 : * away from the sun, but it checks the shortest bounding
505 : * box size rather than the collision radius to avoid
506 : * getting weird shadowing effects around large planar
507 : * entities like the OXP Torus Station.
508 : *
509 : * Well... more weird shadowing effects than there already
510 : * are, anyway.
511 : *
512 : * There are more accurate ways to check "sphere inside
513 : * bounding box" but this seems accurate enough and is
514 : * simpler.
515 : *
516 : * - CIM
517 : */
518 : if (d2_e2 < minbb * minbb)
519 : {
520 : *outValue = 0.1;
521 : return YES;
522 : }
523 : }
524 : return NO;
525 : }
526 :
527 : GLfloat cr_sun = the_sun->collision_radius;
528 :
529 : GLfloat cr2_sun_scaled = cr_sun * cr_sun * d2_e2 / d2_sun;
530 : if (cr_e2 * cr_e2 < cr2_sun_scaled)
531 : {
532 : // if solar disc projected to the distance of e2 > collision radius it can't be shaded by e2
533 : return NO;
534 : }
535 :
536 : // check angles subtended by sun and occluder
537 : // double theta_sun = asin( cr_sun / sqrt(d2_sun)); // 1/2 angle subtended by sun
538 : // double theta_e2 = asin( cr_e2 / sqrt(d2_e2)); // 1/2 angle subtended by e2
539 : // find the difference between the angles subtended by occluder and sun
540 : float d2_e = sqrt(d2_e2);
541 : float theta_diff;
542 : if (d2_e < cr_e2)
543 : {
544 : // then we're "inside" the object. Calculate as if we were on
545 : // the edge of it to avoid taking asin(x>1)
546 : theta_diff = asin(1) - asin(cr_sun / sqrt(d2_sun));
547 : }
548 : else
549 : {
550 : theta_diff = asin(cr_e2 / d2_e) - asin(cr_sun / sqrt(d2_sun));
551 : }
552 :
553 : HPVector p_sun = the_sun->position;
554 : HPVector p_e2 = e2->position;
555 : HPVector p_e1 = e1pos;
556 : Vector v_sun = HPVectorToVector(HPvector_subtract(p_sun, p_e1));
557 : v_sun = vector_normal_or_zbasis(v_sun);
558 :
559 : Vector v_e2 = HPVectorToVector(HPvector_subtract(p_e2, p_e1));
560 : v_e2 = vector_normal_or_xbasis(v_e2);
561 :
562 : float phi = acos(dot_product(v_sun, v_e2)); // angle between sun and e2 from e1's viewpoint
563 : *outValue = (phi / theta_diff); // 1 means just occluded, < 1 means in shadow
564 :
565 : if (phi > theta_diff)
566 : {
567 : // sun is not occluded
568 : return NO;
569 : }
570 :
571 : // all tests done e1 is in shade!
572 : return YES;
573 : }
574 :
575 :
576 0 : static inline BOOL testEntityOccludedByEntity(Entity *e1, Entity *e2, OOSunEntity *the_sun)
577 : {
578 : float tmp; // we're not interested in the amount of occlusion just now.
579 : return entityByEntityOcclusionToValue(e1, e2, the_sun, &tmp);
580 : }
581 :
582 :
583 : - (void) findShadowedEntities
584 : {
585 : // reject trivial cases
586 : if (n_entities < 2) return;
587 :
588 : //
589 : // Copy/pasting the collision code to detect occlusion!
590 : //
591 : unsigned i, j;
592 :
593 : if ([UNIVERSE reducedDetail]) return; // don't do this in reduced detail mode
594 :
595 : OOSunEntity* the_sun = [UNIVERSE sun];
596 :
597 : if (the_sun == nil)
598 : {
599 : return; // sun is required
600 : }
601 :
602 : unsigned ent_count = UNIVERSE->n_entities;
603 : Entity **uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
604 : Entity *planets[ent_count];
605 : unsigned n_planets = 0;
606 : Entity *ships[ent_count];
607 : unsigned n_ships = 0;
608 :
609 : for (i = 0; i < ent_count; i++)
610 : {
611 : if (uni_entities[i]->isSunlit)
612 : {
613 : // get a list of planet entities because they can shade across regions
614 : if ([uni_entities[i] isPlanet])
615 : {
616 : // don't bother retaining - nothing will happen to them!
617 : planets[n_planets++] = uni_entities[i];
618 : }
619 :
620 : // and a list of shipentities large enough that they might cast a noticeable shadow
621 : // if we can't see it, it can't be shadowing anything important
622 : else if ([uni_entities[i] isShip] &&
623 : [uni_entities[i] isVisible] &&
624 : uni_entities[i]->collision_radius >= MINIMUM_SHADOWING_ENTITY_RADIUS)
625 : {
626 : ships[n_ships++] = uni_entities[i]; // don't bother retaining - nothing will happen to them!
627 : }
628 : }
629 : }
630 :
631 : // test for shadows in each subregion
632 : [subregions makeObjectsPerformSelector:@selector(findShadowedEntities)];
633 :
634 : // test each entity in this region against the others
635 : for (i = 0; i < n_entities; i++)
636 : {
637 : Entity *e1 = entity_array[i];
638 : if (![e1 isVisible])
639 : {
640 : continue; // don't check shading of objects we can't see
641 : }
642 : BOOL occluder_moved = NO;
643 : if ([e1 status] == STATUS_COCKPIT_DISPLAY)
644 : {
645 : e1->isSunlit = YES;
646 : e1->shadingEntityID = NO_TARGET;
647 : continue; // don't check shading in demo mode
648 : }
649 : Entity *occluder = nil;
650 : if (e1->isSunlit == NO)
651 : {
652 : occluder = [UNIVERSE entityForUniversalID:e1->shadingEntityID];
653 : if (occluder != nil)
654 : {
655 : occluder_moved = occluder->hasMoved;
656 : }
657 : }
658 : if (([e1 isShip] ||[e1 isPlanet]) && (e1->hasMoved || occluder_moved))
659 : {
660 : e1->isSunlit = YES; // sunlit by default
661 : e1->shadingEntityID = NO_TARGET;
662 : //
663 : // check demo mode here..
664 : if ([e1 isPlayer] && ([(PlayerEntity*)e1 showDemoShips]))
665 : {
666 : continue; // don't check shading in demo mode
667 : }
668 :
669 : // test last occluder (most likely case)
670 : if (occluder)
671 : {
672 : if (testEntityOccludedByEntity(e1, occluder, the_sun))
673 : {
674 : e1->isSunlit = NO;
675 : e1->shadingEntityID = [occluder universalID];
676 : }
677 : }
678 : if (!e1->isSunlit)
679 : {
680 : // no point in continuing tests
681 : continue;
682 : }
683 :
684 : // test planets
685 : for (j = 0; j < n_planets; j++)
686 : {
687 : float occlusionNumber;
688 : if (entityByEntityOcclusionToValue(e1, planets[j], the_sun, &occlusionNumber))
689 : {
690 : e1->isSunlit = NO;
691 : e1->shadingEntityID = [planets[j] universalID];
692 : break;
693 : }
694 : if ([e1 isPlayer])
695 : {
696 : [(PlayerEntity *)e1 setOcclusionLevel:occlusionNumber];
697 : }
698 : }
699 : if (!e1->isSunlit)
700 : {
701 : // no point in continuing tests
702 : continue;
703 : }
704 :
705 : // test local entities
706 : for (j = 0; j < n_ships; j++)
707 : {
708 : if (testEntityOccludedByEntity(e1, ships[j], the_sun))
709 : {
710 : e1->isSunlit = NO;
711 : e1->shadingEntityID = [ships[j] universalID];
712 : break;
713 : }
714 : }
715 : }
716 : }
717 : }
718 :
719 :
720 : - (NSString *) collisionDescription
721 : {
722 : return [NSString stringWithFormat:@"p%u - c%u", checks_this_tick, checks_within_range];
723 : }
724 :
725 :
726 : - (NSString *) debugOut
727 : {
728 : NSMutableString *result = [[NSMutableString alloc] initWithFormat:@"%d:", n_entities];
729 : CollisionRegion *sub = nil;
730 : foreach (sub, subregions)
731 : {
732 : [result appendString:[sub debugOut]];
733 : }
734 : return [result autorelease];
735 : }
736 :
737 : @end
|