f3a91425b84a00f7cb1b49c7f68d14f63d6ef545
[supertux.git] / src / world.cpp
1 //  $Id$
2 // 
3 //  SuperTux
4 //  Copyright (C) 2000 Bill Kendrick <bill@newbreedsoftware.com>
5 //  Copyright (C) 2004 Tobias Glaesser <tobi.web@gmx.de>
6 //  Copyright (C) 2004 Ingo Ruhnke <grumbel@gmx.de>
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., 59 Temple Place - Suite 330, Boston, MA
21 //  02111-1307, USA.
22
23 #include <iostream>
24 #include <math.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include "globals.h"
28 #include "scene.h"
29 #include "screen.h"
30 #include "defines.h"
31 #include "world.h"
32 #include "level.h"
33 #include "tile.h"
34 #include "resources.h"
35 #include "gameobjs.h"
36 #include "viewport.h"
37 #include "display_manager.h"
38 #include "background.h"
39 #include "tilemap.h"
40
41 Surface* img_distro[4];
42
43 World* World::current_ = 0;
44
45 World::World(const std::string& filename)
46 {
47   // FIXME: Move this to action and draw and everywhere else where the
48   // world calls child functions
49   current_ = this;
50
51   level = new Level();
52   level->load(filename, this);
53
54   tux = new Player(displaymanager);
55   gameobjects.push_back(tux);
56
57   set_defaults();
58
59   get_level()->load_gfx();
60   // add background
61   activate_particle_systems();
62   Background* bg = new Background(displaymanager);
63   if(level->img_bkgd) {
64     bg->set_image(level->img_bkgd, level->bkgd_speed);
65   } else {
66     bg->set_gradient(level->bkgd_top, level->bkgd_bottom);
67   }
68   gameobjects.push_back(bg);
69
70   // add tilemap
71   gameobjects.push_back(new TileMap(displaymanager, get_level()));
72   activate_objects();
73   get_level()->load_song();
74
75   apply_bonuses();
76
77   scrolling_timer.init(true);
78 }
79
80 World::World(const std::string& subset, int level_nr)
81 {
82   // FIXME: Move this to action and draw and everywhere else where the
83   // world calls child functions
84   current_ = this;
85
86   level = new Level();
87   level->load(subset, level_nr, this);
88
89   tux = new Player(displaymanager);
90   gameobjects.push_back(tux);        
91
92   set_defaults();
93
94   get_level()->load_gfx();
95   activate_objects();
96   activate_particle_systems();
97   Background* bg = new Background(displaymanager);
98   if(level->img_bkgd) {
99     bg->set_image(level->img_bkgd, level->bkgd_speed);
100   } else {
101     bg->set_gradient(level->bkgd_top, level->bkgd_bottom);
102   }
103   gameobjects.push_back(bg);
104   // add tilemap
105   gameobjects.push_back(new TileMap(displaymanager, get_level()));  
106   get_level()->load_song();
107
108   apply_bonuses();
109
110   scrolling_timer.init(true);
111 }
112
113 void
114 World::apply_bonuses()
115 {
116   // Apply bonuses from former levels
117   switch (player_status.bonus)
118     {
119     case PlayerStatus::NO_BONUS:
120       break;
121                                                                                 
122     case PlayerStatus::FLOWER_BONUS:
123       tux->got_power = Player::FIRE_POWER;  // FIXME: add ice power to here
124       // fall through
125                                                                                 
126     case PlayerStatus::GROWUP_BONUS:
127       tux->grow();
128       break;
129     }
130 }
131
132 World::~World()
133 {
134   for (Trampolines::iterator i = trampolines.begin(); i != trampolines.end(); ++i)
135     delete *i;
136
137   for (std::vector<_GameObject*>::iterator i = gameobjects.begin();
138           i != gameobjects.end(); ++i) {
139     Drawable* drawable = dynamic_cast<Drawable*> (*i);
140     if(drawable)
141       displaymanager.remove_drawable(drawable);
142     delete *i;
143   }
144   bad_guys.clear();
145
146   delete level;
147 }
148
149 void
150 World::set_defaults()
151 {
152   // Set defaults: 
153   scroll_x = 0;
154
155   player_status.score_multiplier = 1;
156
157   counting_distros = false;
158   distro_counter = 0;
159
160   /* set current song/music */
161   currentmusic = LEVEL_MUSIC;
162 }
163
164 void
165 World::add_object(_GameObject* object)
166 {
167   // XXX hack for now until new collision code is ready
168   BadGuy* badguy = dynamic_cast<BadGuy*> (object);
169   if(badguy)
170     bad_guys.push_back(badguy);
171
172   gameobjects.push_back(object);
173 }
174
175 void
176 World::parse_objects(lisp_object_t* cur)
177 {
178   while(!lisp_nil_p(cur)) {
179     lisp_object_t* data = lisp_car(cur);
180     std::string object_type = "";
181     
182     LispReader reader(lisp_cdr(data));
183     reader.read_string("type", &object_type);
184
185     if(object_type == "badguy" || object_type == "")
186     {
187       BadGuyKind kind = badguykind_from_string(
188           lisp_symbol(lisp_car(data)));
189       add_object(new BadGuy(displaymanager, kind, reader));
190     }
191     // TODO add parsing code for trampolines
192
193     cur = lisp_cdr(cur);
194   } 
195 }
196
197 void
198 World::activate_objects()
199 {
200   for (std::vector< ObjectData<TrampolineData> >::iterator i = level->trampoline_data.begin();
201        i != level->trampoline_data.end();
202        ++i)
203   {
204     add_object<Trampoline, ObjectData<TrampolineData> >(*i);
205   }
206 }
207
208 void
209 World::activate_particle_systems()
210 {
211   if (level->particle_system == "clouds")
212     {
213       add_object(new CloudParticleSystem(displaymanager));
214     }
215   else if (level->particle_system == "snow")
216     {
217       add_object(new SnowParticleSystem(displaymanager));
218     }
219   else if (level->particle_system != "")
220     {
221       st_abort("unknown particle system specified in level", "");
222     }
223 }
224
225 void
226 World::draw()
227 {
228   /* Draw objects */
229   displaymanager.get_viewport().set_translation(Vector(scroll_x, scroll_y));
230   displaymanager.draw();
231   
232   for (Trampolines::iterator i = trampolines.begin(); i != trampolines.end(); ++i)
233     (*i)->draw();
234
235   for (unsigned int i = 0; i < bullets.size(); ++i)
236     bullets[i].draw();
237
238   for (unsigned int i = 0; i < upgrades.size(); ++i)
239     upgrades[i].draw();
240 }
241
242 void
243 World::action(double frame_ratio)
244 {
245   tux->check_bounds(level->back_scrolling, (bool)level->hor_autoscroll_speed);
246   scrolling(frame_ratio);
247
248   for (unsigned int i = 0; i < bullets.size(); ++i)
249     bullets[i].action(frame_ratio);
250   
251   for (unsigned int i = 0; i < upgrades.size(); i++)
252     upgrades[i].action(frame_ratio);
253
254   for (Trampolines::iterator i = trampolines.begin(); i != trampolines.end(); ++i)
255      (*i)->action(frame_ratio);
256
257   /* update objects (don't use iterators here, because the list might change
258    * during the iteration)
259    */
260   for(size_t i = 0; i < gameobjects.size(); ++i)
261     gameobjects[i]->action(frame_ratio);
262
263   /* Handle all possible collisions. */
264   collision_handler();
265  
266   /** cleanup marked objects */
267   for(std::vector<_GameObject*>::iterator i = gameobjects.begin();
268       i != gameobjects.end(); /* nothing */) {
269     if((*i)->is_valid() == false) {
270       Drawable* drawable = dynamic_cast<Drawable*> (*i);
271       if(drawable)
272         displaymanager.remove_drawable(drawable);
273       BadGuy* badguy = dynamic_cast<BadGuy*> (*i);
274       if(badguy) {
275         std::remove(bad_guys.begin(), bad_guys.end(), badguy);
276       } 
277       
278       delete *i;
279       i = gameobjects.erase(i);
280     } else {
281       ++i;
282     }
283   }
284 }
285
286 /* the space that it takes for the screen to start scrolling, regarding */
287 /* screen bounds (in pixels) */
288 // should be higher than screen->w/2 (400)
289 #define X_SPACE (500-16)
290 // should be less than screen->h/2 (300)
291 #define Y_SPACE 250
292
293 // the time it takes to move the camera (in ms)
294 #define CHANGE_DIR_SCROLL_SPEED 2000
295
296 /* This functions takes cares of the scrolling */
297 void World::scrolling(double frame_ratio)
298 {
299   /* Y-axis scrolling */
300
301   float tux_pos_y = tux->base.y + (tux->base.height/2);
302
303   if(level->height > VISIBLE_TILES_Y-1 && !tux->dying)
304     {
305     if (scroll_y < tux_pos_y - (screen->h - Y_SPACE))
306       scroll_y = tux_pos_y - (screen->h - Y_SPACE);
307     else if (scroll_y > tux_pos_y - Y_SPACE)
308       scroll_y = tux_pos_y - Y_SPACE;
309     }
310
311   // this code prevent the screen to scroll before the start or after the level's end
312   if(scroll_y > level->height * 32 - screen->h)
313     scroll_y = level->height * 32 - screen->h;
314   if(scroll_y < 0)
315     scroll_y = 0;
316
317   /* X-axis scrolling */
318
319   /* Auto scrolling */
320   if(level->hor_autoscroll_speed)
321   {
322     scroll_x += level->hor_autoscroll_speed * frame_ratio;
323     return;
324   }
325
326
327   /* Horizontal backscrolling */
328   float tux_pos_x = tux->base.x + (tux->base.width/2);
329
330   if(tux->old_dir != tux->dir && level->back_scrolling)
331     scrolling_timer.start(CHANGE_DIR_SCROLL_SPEED);
332
333   bool right = false;
334   bool left = false;
335   if (tux->physic.get_velocity_x() > 0)
336     right = true;
337   else if (tux->physic.get_velocity_x() < 0)
338     left = true;
339   else
340     {
341     if (tux->dir == RIGHT)
342       right = true;
343     else
344       left = true;
345     }
346
347   if(scrolling_timer.check())
348   {
349     float final_scroll_x;
350     float constant1;
351     float constant2;
352     if (right)
353       final_scroll_x = tux_pos_x - (screen->w - X_SPACE);
354     else
355       final_scroll_x = tux_pos_x - X_SPACE;
356
357     if((tux->physic.get_velocity_x() > 0 && tux->dir == RIGHT)
358         || (tux->physic.get_velocity_x() < 0 && tux->dir == LEFT))
359     {
360       constant1 = 1.0;
361       constant2 = .4;
362     }
363     else
364     {
365       constant1 = 0.;
366       constant2 = 0.;
367     }
368     
369     float number = 2.5/(frame_ratio * CHANGE_DIR_SCROLL_SPEED/1000)*exp((CHANGE_DIR_SCROLL_SPEED-scrolling_timer.get_left())/1400.);
370     if(left) number *= -1.;
371
372     scroll_x += number
373             + constant1 * tux->physic.get_velocity_x() * frame_ratio
374             + constant2 * tux->physic.get_acceleration_x() * frame_ratio * frame_ratio;
375
376     if ((right && final_scroll_x - scroll_x < 0) || (left && final_scroll_x - scroll_x > 0))
377       scroll_x = final_scroll_x;
378     
379   }
380   else
381   {
382     if (right && scroll_x < tux_pos_x - (screen->w - X_SPACE))
383       scroll_x = tux_pos_x - (screen->w - X_SPACE);
384     else if (left && scroll_x > tux_pos_x - X_SPACE && level->back_scrolling)
385       scroll_x = tux_pos_x - X_SPACE;
386   }
387
388   // this code prevent the screen to scroll before the start or after the level's end
389   if(scroll_x > level->width * 32 - screen->w)
390     scroll_x = level->width * 32 - screen->w;
391   if(scroll_x < 0)
392     scroll_x = 0;
393 }
394
395 void
396 World::collision_handler()
397 {
398   // CO_BULLET & CO_BADGUY check
399   for(unsigned int i = 0; i < bullets.size(); ++i)
400     {
401       for (BadGuys::iterator j = bad_guys.begin(); j != bad_guys.end(); ++j)
402         {
403           if((*j)->dying != DYING_NOT)
404             continue;
405           
406           if(rectcollision(bullets[i].base, (*j)->base))
407             {
408               // We have detected a collision and now call the
409               // collision functions of the collided objects.
410               // collide with bad_guy first, since bullet_collision will
411               // delete the bullet
412               (*j)->collision(&bullets[i], CO_BULLET);
413               bullets[i].collision(CO_BADGUY);
414               break; // bullet is invalid now, so break
415             }
416         }
417     }
418
419   /* CO_BADGUY & CO_BADGUY check */
420   for (BadGuys::iterator i = bad_guys.begin(); i != bad_guys.end(); ++i)
421     {
422       if((*i)->dying != DYING_NOT)
423         continue;
424       
425       BadGuys::iterator j = i;
426       ++j;
427       for (; j != bad_guys.end(); ++j)
428         {
429           if(j == i || (*j)->dying != DYING_NOT)
430             continue;
431
432           if(rectcollision((*i)->base, (*j)->base))
433             {
434               // We have detected a collision and now call the
435               // collision functions of the collided objects.
436               (*j)->collision(*i, CO_BADGUY);
437               (*i)->collision(*j, CO_BADGUY);
438             }
439         }
440     }
441
442   if(tux->dying != DYING_NOT) return;
443     
444   // CO_BADGUY & CO_PLAYER check 
445   for (BadGuys::iterator i = bad_guys.begin(); i != bad_guys.end(); ++i)
446     {
447       if((*i)->dying != DYING_NOT)
448         continue;
449       
450       if(rectcollision_offset((*i)->base, tux->base, 0, 0))
451         {
452           // We have detected a collision and now call the collision
453           // functions of the collided objects.
454           if (tux->previous_base.y < tux->base.y &&
455               tux->previous_base.y + tux->previous_base.height 
456               < (*i)->base.y + (*i)->base.height/2
457               && !tux->invincible_timer.started())
458             {
459               (*i)->collision(tux, CO_PLAYER, COLLISION_SQUISH);
460             }
461           else
462             {
463               tux->collision(*i, CO_BADGUY);
464               (*i)->collision(tux, CO_PLAYER, COLLISION_NORMAL);
465             }
466         }
467     }
468
469   // CO_UPGRADE & CO_PLAYER check
470   for(unsigned int i = 0; i < upgrades.size(); ++i)
471     {
472       if(rectcollision(upgrades[i].base, tux->base))
473         {
474           // We have detected a collision and now call the collision
475           // functions of the collided objects.
476           upgrades[i].collision(tux, CO_PLAYER, COLLISION_NORMAL);
477         }
478     }
479
480   // CO_TRAMPOLINE & (CO_PLAYER or CO_BADGUY)
481   for (Trampolines::iterator i = trampolines.begin(); i != trampolines.end(); ++i)
482   {
483     if (rectcollision((*i)->base, tux->base))
484     {
485       if (tux->previous_base.y < tux->base.y &&
486           tux->previous_base.y + tux->previous_base.height 
487           < (*i)->base.y + (*i)->base.height/2)
488       {
489         (*i)->collision(tux, CO_PLAYER, COLLISION_SQUISH);
490       }
491       else if (tux->previous_base.y <= tux->base.y)
492       {
493         tux->collision(*i, CO_TRAMPOLINE);
494         (*i)->collision(tux, CO_PLAYER, COLLISION_NORMAL);
495       }
496     }
497   }
498 }
499
500 void
501 World::add_score(const Vector& pos, int s)
502 {
503   player_status.score += s;
504
505   add_object(new FloatingScore(displaymanager, pos, s));
506 }
507
508 void
509 World::add_bouncy_distro(const Vector& pos)
510 {
511   add_object(new BouncyDistro(displaymanager, pos));
512 }
513
514 void
515 World::add_broken_brick(const Vector& pos, Tile* tile)
516 {
517   add_broken_brick_piece(pos, Vector(-1, -4), tile);
518   add_broken_brick_piece(pos + Vector(0, 16), Vector(-1.5, -3), tile);
519
520   add_broken_brick_piece(pos + Vector(16, 0), Vector(1, -4), tile);
521   add_broken_brick_piece(pos + Vector(16, 16), Vector(1.5, -3), tile);
522 }
523
524 void
525 World::add_broken_brick_piece(const Vector& pos, const Vector& movement,
526     Tile* tile)
527 {
528   add_object(new BrokenBrick(displaymanager, tile, pos, movement));
529 }
530
531 void
532 World::add_bouncy_brick(const Vector& pos)
533 {
534   add_object(new BouncyBrick(displaymanager, pos));
535 }
536
537 BadGuy*
538 World::add_bad_guy(float x, float y, BadGuyKind kind)
539 {
540   BadGuy* badguy = new BadGuy(displaymanager, kind, x, y);
541   add_object(badguy);
542   return badguy;
543 }
544
545 template<class T, class U>
546 T*
547 World::add_object(U data)
548 {
549   T* tobject = new T(data);
550
551   if (data.type == OBJ_TRAMPOLINE)
552     trampolines.push_back(tobject);
553
554   return tobject;
555 }
556
557 void
558 World::add_upgrade(float x, float y, Direction dir, UpgradeKind kind)
559 {
560   Upgrade new_upgrade;
561   new_upgrade.init(x,y,dir,kind);
562   upgrades.push_back(new_upgrade);
563 }
564
565 void 
566 World::add_bullet(float x, float y, float xm, Direction dir)
567 {
568   if(tux->got_power == Player::FIRE_POWER)
569     {
570     if(bullets.size() > MAX_FIRE_BULLETS-1)
571       return;
572     }
573   else if(tux->got_power == Player::ICE_POWER)
574     {
575     if(bullets.size() > MAX_ICE_BULLETS-1)
576       return;
577     }
578
579   Bullet new_bullet;
580   if(tux->got_power == Player::FIRE_POWER)
581     new_bullet.init(x,y,xm,dir, FIRE_BULLET);
582   else if(tux->got_power == Player::ICE_POWER)
583     new_bullet.init(x,y,xm,dir, ICE_BULLET);
584   bullets.push_back(new_bullet);
585   
586   play_sound(sounds[SND_SHOOT], SOUND_CENTER_SPEAKER);
587 }
588
589 void
590 World::play_music(int musictype)
591 {
592   currentmusic = musictype;
593   switch(currentmusic) {
594     case HURRYUP_MUSIC:
595       music_manager->play_music(get_level()->get_level_music_fast());
596       break;
597     case LEVEL_MUSIC:
598       music_manager->play_music(get_level()->get_level_music());
599       break;
600     case HERRING_MUSIC:
601       music_manager->play_music(herring_song);
602       break;
603     default:
604       music_manager->halt_music();
605       break;
606   }
607 }
608
609 int
610 World::get_music_type()
611 {
612   return currentmusic;
613 }
614
615 /* Break a brick: */
616 bool
617 World::trybreakbrick(float x, float y, bool small)
618 {
619   Level* plevel = get_level();
620   
621   Tile* tile = gettile(x, y);
622   if (tile->brick)
623     {
624       if (tile->data > 0)
625         {
626           /* Get a distro from it: */
627           add_bouncy_distro(
628               Vector(((int)(x + 1) / 32) * 32, (int)(y / 32) * 32));
629
630           // TODO: don't handle this in a global way but per-tile...
631           if (!counting_distros)
632             {
633               counting_distros = true;
634               distro_counter = 5;
635             }
636           else
637             {
638               distro_counter--;
639             }
640
641           if (distro_counter <= 0)
642             {
643               counting_distros = false;
644               plevel->change(x, y, TM_IA, tile->next_tile);
645             }
646
647           play_sound(sounds[SND_DISTRO], SOUND_CENTER_SPEAKER);
648           player_status.score = player_status.score + SCORE_DISTRO;
649           player_status.distros++;
650           return true;
651         }
652       else if (!small)
653         {
654           /* Get rid of it: */
655           plevel->change(x, y, TM_IA, tile->next_tile);
656           
657           /* Replace it with broken bits: */
658           add_broken_brick(Vector(
659                                  ((int)(x + 1) / 32) * 32,
660                                  (int)(y / 32) * 32), tile);
661           
662           /* Get some score: */
663           play_sound(sounds[SND_BRICK], SOUND_CENTER_SPEAKER);
664           player_status.score = player_status.score + SCORE_BRICK;
665           
666           return true;
667         }
668     }
669
670   return false;
671 }
672
673 /* Empty a box: */
674 void
675 World::tryemptybox(float x, float y, Direction col_side)
676 {
677   Tile* tile = gettile(x,y);
678   if (!tile->fullbox)
679     return;
680
681   // according to the collision side, set the upgrade direction
682   if(col_side == LEFT)
683     col_side = RIGHT;
684   else
685     col_side = LEFT;
686
687   int posx = ((int)(x+1) / 32) * 32;
688   int posy = (int)(y/32) * 32 - 32;
689   switch(tile->data)
690     {
691     case 1: // Box with a distro!
692       add_bouncy_distro(Vector(posx, posy));
693       play_sound(sounds[SND_DISTRO], SOUND_CENTER_SPEAKER);
694       player_status.score = player_status.score + SCORE_DISTRO;
695       player_status.distros++;
696       break;
697
698     case 2: // Add a fire flower upgrade!
699       if (tux->size == SMALL)     /* Tux is small, add mints! */
700         add_upgrade(posx, posy, col_side, UPGRADE_GROWUP);
701       else     /* Tux is big, add a fireflower: */
702         add_upgrade(posx, posy, col_side, UPGRADE_FIREFLOWER);
703       play_sound(sounds[SND_UPGRADE], SOUND_CENTER_SPEAKER);
704       break;
705     
706     case 5: // Add an ice flower upgrade!
707       if (tux->size == SMALL)     /* Tux is small, add mints! */
708         add_upgrade(posx, posy, col_side, UPGRADE_GROWUP);
709       else     /* Tux is big, add an iceflower: */
710         add_upgrade(posx, posy, col_side, UPGRADE_ICEFLOWER);
711       play_sound(sounds[SND_UPGRADE], SOUND_CENTER_SPEAKER);
712       break;
713
714     case 3: // Add a golden herring
715       add_upgrade(posx, posy, col_side, UPGRADE_HERRING);
716       break;
717
718     case 4: // Add a 1up extra
719       add_upgrade(posx, posy, col_side, UPGRADE_1UP);
720       break;
721     default:
722       break;
723     }
724
725   /* Empty the box: */
726   level->change(x, y, TM_IA, tile->next_tile);
727 }
728
729 /* Try to grab a distro: */
730 void
731 World::trygrabdistro(float x, float y, int bounciness)
732 {
733   Tile* tile = gettile(x, y);
734   if (tile && tile->distro)
735     {
736       level->change(x, y, TM_IA, tile->next_tile);
737       play_sound(sounds[SND_DISTRO], SOUND_CENTER_SPEAKER);
738
739       if (bounciness == BOUNCE)
740         {
741           add_bouncy_distro(Vector(((int)(x + 1) / 32) * 32,
742                                   (int)(y / 32) * 32));
743         }
744
745       player_status.score = player_status.score + SCORE_DISTRO;
746       player_status.distros++;
747     }
748 }
749
750 /* Try to bump a bad guy from below: */
751 void
752 World::trybumpbadguy(float x, float y)
753 {
754   // Bad guys: 
755   for (BadGuys::iterator i = bad_guys.begin(); i != bad_guys.end(); ++i)
756     {
757       if ((*i)->base.x >= x - 32 && (*i)->base.x <= x + 32 &&
758           (*i)->base.y >= y - 16 && (*i)->base.y <= y + 16)
759         {
760           (*i)->collision(tux, CO_PLAYER, COLLISION_BUMP);
761         }
762     }
763
764   // Upgrades:
765   for (unsigned int i = 0; i < upgrades.size(); i++)
766     {
767       if (upgrades[i].base.height == 32 &&
768           upgrades[i].base.x >= x - 32 && upgrades[i].base.x <= x + 32 &&
769           upgrades[i].base.y >= y - 16 && upgrades[i].base.y <= y + 16)
770         {
771           upgrades[i].collision(tux, CO_PLAYER, COLLISION_BUMP);
772         }
773     }
774 }
775
776 /* EOF */
777